feat: новая функция
This commit is contained in:
@@ -27,29 +27,62 @@ router.get('/', async (req, res) => {
|
|||||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Проверяем, это гостевой идентификатор (формат: channel:rawId)
|
// Проверяем, это гостевой идентификатор (формат: guest_123)
|
||||||
if (userId && userId.includes(':')) {
|
if (userId && userId.startsWith('guest_')) {
|
||||||
|
const guestId = parseInt(userId.replace('guest_', ''));
|
||||||
|
|
||||||
|
if (isNaN(guestId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid guest ID format' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сначала получаем guest_identifier по guestId
|
||||||
|
const identifierResult = await db.getQuery()(
|
||||||
|
`WITH decrypted_guest AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
decrypt_text(identifier_encrypted, $2) as guest_identifier,
|
||||||
|
channel
|
||||||
|
FROM unified_guest_messages
|
||||||
|
WHERE user_id IS NULL
|
||||||
|
)
|
||||||
|
SELECT guest_identifier, channel
|
||||||
|
FROM decrypted_guest
|
||||||
|
GROUP BY guest_identifier, channel
|
||||||
|
HAVING MIN(id) = $1
|
||||||
|
LIMIT 1`,
|
||||||
|
[guestId, encryptionKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (identifierResult.rows.length === 0) {
|
||||||
|
return res.json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guestIdentifier = identifierResult.rows[0].guest_identifier;
|
||||||
|
const guestChannel = identifierResult.rows[0].channel;
|
||||||
|
|
||||||
|
// Теперь получаем все сообщения этого гостя (по идентификатору И каналу)
|
||||||
const guestResult = await db.getQuery()(
|
const guestResult = await db.getQuery()(
|
||||||
`SELECT
|
`SELECT
|
||||||
id,
|
id,
|
||||||
decrypt_text(identifier_encrypted, $2) as user_id,
|
decrypt_text(identifier_encrypted, $3) as user_id,
|
||||||
channel,
|
channel,
|
||||||
decrypt_text(content_encrypted, $2) as content,
|
decrypt_text(content_encrypted, $3) as content,
|
||||||
content_type,
|
content_type,
|
||||||
attachments,
|
attachments,
|
||||||
media_metadata,
|
media_metadata,
|
||||||
is_ai,
|
is_ai,
|
||||||
created_at
|
created_at
|
||||||
FROM unified_guest_messages
|
FROM unified_guest_messages
|
||||||
WHERE decrypt_text(identifier_encrypted, $2) = $1
|
WHERE decrypt_text(identifier_encrypted, $3) = $1
|
||||||
|
AND channel = $2
|
||||||
ORDER BY created_at ASC`,
|
ORDER BY created_at ASC`,
|
||||||
[userId, encryptionKey]
|
[guestIdentifier, guestChannel, encryptionKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Преобразуем формат для совместимости с фронтендом
|
// Преобразуем формат для совместимости с фронтендом
|
||||||
const messages = guestResult.rows.map(msg => ({
|
const messages = guestResult.rows.map(msg => ({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
user_id: msg.user_id,
|
user_id: `guest_${guestId}`,
|
||||||
sender_type: msg.is_ai ? 'bot' : 'user',
|
sender_type: msg.is_ai ? 'bot' : 'user',
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
channel: msg.channel,
|
channel: msg.channel,
|
||||||
|
|||||||
@@ -25,11 +25,24 @@ router.use((req, res, next) => {
|
|||||||
|
|
||||||
// PATCH /api/tags/user/:userId — установить теги пользователю
|
// PATCH /api/tags/user/:userId — установить теги пользователю
|
||||||
router.patch('/user/:userId', async (req, res) => {
|
router.patch('/user/:userId', async (req, res) => {
|
||||||
const userId = Number(req.params.userId);
|
const userIdParam = req.params.userId;
|
||||||
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
|
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
|
||||||
|
|
||||||
|
// Гостевые пользователи (guest_123) не могут иметь теги
|
||||||
|
if (userIdParam.startsWith('guest_')) {
|
||||||
|
return res.status(400).json({ error: 'Guests cannot have tags' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = Number(userIdParam);
|
||||||
|
|
||||||
|
if (isNaN(userId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid user ID' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(tags)) {
|
if (!Array.isArray(tags)) {
|
||||||
return res.status(400).json({ error: 'tags должен быть массивом' });
|
return res.status(400).json({ error: 'tags должен быть массивом' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Удаляем старые связи
|
// Удаляем старые связи
|
||||||
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
|
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
|
||||||
@@ -52,7 +65,19 @@ router.patch('/user/:userId', async (req, res) => {
|
|||||||
|
|
||||||
// GET /api/tags/user/:userId — получить все теги пользователя
|
// GET /api/tags/user/:userId — получить все теги пользователя
|
||||||
router.get('/user/:userId', async (req, res) => {
|
router.get('/user/:userId', async (req, res) => {
|
||||||
const userId = Number(req.params.userId);
|
const userIdParam = req.params.userId;
|
||||||
|
|
||||||
|
// Гостевые пользователи (guest_123) не имеют тегов
|
||||||
|
if (userIdParam.startsWith('guest_')) {
|
||||||
|
return res.json({ tags: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = Number(userIdParam);
|
||||||
|
|
||||||
|
if (isNaN(userId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid user ID' });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.getQuery()(
|
const result = await db.getQuery()(
|
||||||
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
|
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
|
||||||
@@ -66,8 +91,20 @@ router.get('/user/:userId', async (req, res) => {
|
|||||||
|
|
||||||
// DELETE /api/tags/user/:userId/tag/:tagId — удалить тег у пользователя
|
// DELETE /api/tags/user/:userId/tag/:tagId — удалить тег у пользователя
|
||||||
router.delete('/user/:userId/tag/:tagId', async (req, res) => {
|
router.delete('/user/:userId/tag/:tagId', async (req, res) => {
|
||||||
const userId = Number(req.params.userId);
|
const userIdParam = req.params.userId;
|
||||||
|
|
||||||
|
// Гостевые пользователи (guest_123) не могут иметь теги
|
||||||
|
if (userIdParam.startsWith('guest_')) {
|
||||||
|
return res.status(400).json({ error: 'Guests cannot have tags' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = Number(userIdParam);
|
||||||
const tagId = Number(req.params.tagId);
|
const tagId = Number(req.params.tagId);
|
||||||
|
|
||||||
|
if (isNaN(userId) || isNaN(tagId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid user ID or tag ID' });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.getQuery()(
|
await db.getQuery()(
|
||||||
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
||||||
|
|||||||
@@ -226,26 +226,39 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
const guestContactsResult = await db.getQuery()(
|
const guestContactsResult = await db.getQuery()(
|
||||||
`WITH decrypted_guests AS (
|
`WITH decrypted_guests AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
id,
|
||||||
decrypt_text(identifier_encrypted, $1) as guest_identifier,
|
decrypt_text(identifier_encrypted, $1) as guest_identifier,
|
||||||
channel,
|
channel,
|
||||||
created_at,
|
created_at,
|
||||||
user_id
|
user_id
|
||||||
FROM unified_guest_messages
|
FROM unified_guest_messages
|
||||||
WHERE user_id IS NULL
|
WHERE user_id IS NULL
|
||||||
|
),
|
||||||
|
guest_groups AS (
|
||||||
|
SELECT
|
||||||
|
MIN(id) as guest_id,
|
||||||
|
guest_identifier,
|
||||||
|
channel,
|
||||||
|
MIN(created_at) as created_at,
|
||||||
|
MAX(created_at) as last_message_at,
|
||||||
|
COUNT(*) as message_count
|
||||||
|
FROM decrypted_guests
|
||||||
|
GROUP BY guest_identifier, channel
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
ROW_NUMBER() OVER (ORDER BY guest_id ASC) as guest_number,
|
||||||
|
guest_id,
|
||||||
guest_identifier,
|
guest_identifier,
|
||||||
channel,
|
channel,
|
||||||
MIN(created_at) as created_at,
|
created_at,
|
||||||
MAX(created_at) as last_message_at,
|
last_message_at,
|
||||||
COUNT(*) as message_count
|
message_count
|
||||||
FROM decrypted_guests
|
FROM guest_groups
|
||||||
GROUP BY guest_identifier, channel
|
ORDER BY guest_id ASC`,
|
||||||
ORDER BY MAX(created_at) DESC`,
|
|
||||||
[encryptionKey]
|
[encryptionKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
const guestContacts = guestContactsResult.rows.map(g => {
|
const guestContacts = guestContactsResult.rows.map((g) => {
|
||||||
const channelMap = {
|
const channelMap = {
|
||||||
'web': '🌐',
|
'web': '🌐',
|
||||||
'telegram': '📱',
|
'telegram': '📱',
|
||||||
@@ -254,9 +267,21 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
const icon = channelMap[g.channel] || '👤';
|
const icon = channelMap[g.channel] || '👤';
|
||||||
const rawId = g.guest_identifier.replace(`${g.channel}:`, '');
|
const rawId = g.guest_identifier.replace(`${g.channel}:`, '');
|
||||||
|
|
||||||
|
// Формируем имя в зависимости от канала
|
||||||
|
let displayName;
|
||||||
|
if (g.channel === 'email') {
|
||||||
|
displayName = `${icon} ${rawId}`;
|
||||||
|
} else if (g.channel === 'telegram') {
|
||||||
|
displayName = `${icon} Telegram (${rawId})`;
|
||||||
|
} else {
|
||||||
|
displayName = `${icon} Гость ${g.guest_number}`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: g.guest_identifier, // Используем unified identifier как ID
|
id: `guest_${g.guest_id}`, // Используем внутренний ID для поиска
|
||||||
name: `${icon} ${g.channel === 'web' ? 'Гость' : g.channel} (${rawId.substring(0, 8)}...)`,
|
guest_number: parseInt(g.guest_number), // Порядковый номер для отображения
|
||||||
|
guest_identifier: g.guest_identifier, // Сохраняем для запросов
|
||||||
|
name: displayName,
|
||||||
email: g.channel === 'email' ? rawId : null,
|
email: g.channel === 'email' ? rawId : null,
|
||||||
telegram: g.channel === 'telegram' ? rawId : null,
|
telegram: g.channel === 'telegram' ? rawId : null,
|
||||||
wallet: null,
|
wallet: null,
|
||||||
@@ -322,13 +347,27 @@ router.post('/mark-contact-read', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const adminId = req.user && req.user.id;
|
const adminId = req.user && req.user.id;
|
||||||
const { contactId } = req.body;
|
const { contactId } = req.body;
|
||||||
|
|
||||||
if (!adminId || !contactId) {
|
if (!adminId || !contactId) {
|
||||||
return res.status(400).json({ error: 'adminId and contactId required' });
|
return res.status(400).json({ error: 'adminId and contactId required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидация contactId: может быть числом (user.id) или строкой (guest identifier)
|
||||||
|
// Приводим к строке для универсальности
|
||||||
|
const contactIdStr = String(contactId);
|
||||||
|
|
||||||
|
// Проверка на допустимые форматы:
|
||||||
|
// - Число (user.id): "123"
|
||||||
|
// - Гостевой идентификатор: "telegram:123", "email:user@example.com", "web:uuid"
|
||||||
|
if (!contactIdStr || contactIdStr.length > 255) {
|
||||||
|
return res.status(400).json({ error: 'Invalid contactId format' });
|
||||||
|
}
|
||||||
|
|
||||||
await db.query(
|
await db.query(
|
||||||
'INSERT INTO admin_read_contacts (admin_id, contact_id, read_at) VALUES ($1, $2, NOW()) ON CONFLICT (admin_id, contact_id) DO UPDATE SET read_at = NOW()',
|
'INSERT INTO admin_read_contacts (admin_id, contact_id, read_at) VALUES ($1, $2, NOW()) ON CONFLICT (admin_id, contact_id) DO UPDATE SET read_at = NOW()',
|
||||||
[adminId, contactId]
|
[adminId, contactIdStr]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ERROR] /mark-contact-read:', e);
|
console.error('[ERROR] /mark-contact-read:', e);
|
||||||
@@ -426,19 +465,75 @@ router.patch('/:id', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
// DELETE /api/users/:id — удалить контакт и все связанные данные
|
// DELETE /api/users/:id — удалить контакт и все связанные данные
|
||||||
router.delete('/:id', requireAuth, async (req, res) => {
|
router.delete('/:id', requireAuth, async (req, res) => {
|
||||||
// console.log('[users.js] DELETE HANDLER', req.params.id);
|
const userIdParam = req.params.id;
|
||||||
const userId = Number(req.params.id);
|
|
||||||
// console.log('[ROUTER] Перед вызовом deleteUserById для userId:', userId);
|
|
||||||
try {
|
try {
|
||||||
|
// Обработка гостевых контактов (guest_123)
|
||||||
|
if (userIdParam.startsWith('guest_')) {
|
||||||
|
const guestId = parseInt(userIdParam.replace('guest_', ''));
|
||||||
|
|
||||||
|
if (isNaN(guestId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid guest ID format' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ключ шифрования
|
||||||
|
const encryptionUtils = require('../utils/encryptionUtils');
|
||||||
|
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||||
|
|
||||||
|
// Находим guest_identifier по guestId
|
||||||
|
const identifierResult = await db.getQuery()(
|
||||||
|
`WITH decrypted_guest AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
decrypt_text(identifier_encrypted, $2) as guest_identifier,
|
||||||
|
channel
|
||||||
|
FROM unified_guest_messages
|
||||||
|
WHERE user_id IS NULL
|
||||||
|
)
|
||||||
|
SELECT guest_identifier, channel
|
||||||
|
FROM decrypted_guest
|
||||||
|
GROUP BY guest_identifier, channel
|
||||||
|
HAVING MIN(id) = $1
|
||||||
|
LIMIT 1`,
|
||||||
|
[guestId, encryptionKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (identifierResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Guest contact not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const guestIdentifier = identifierResult.rows[0].guest_identifier;
|
||||||
|
const guestChannel = identifierResult.rows[0].channel;
|
||||||
|
|
||||||
|
// Удаляем все сообщения этого гостя
|
||||||
|
const deleteResult = await db.getQuery()(
|
||||||
|
`DELETE FROM unified_guest_messages
|
||||||
|
WHERE decrypt_text(identifier_encrypted, $2) = $1
|
||||||
|
AND channel = $3`,
|
||||||
|
[guestIdentifier, encryptionKey, guestChannel]
|
||||||
|
);
|
||||||
|
|
||||||
|
broadcastContactsUpdate();
|
||||||
|
return res.json({ success: true, deleted: deleteResult.rowCount });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка обычных пользователей
|
||||||
|
const userId = Number(userIdParam);
|
||||||
|
|
||||||
|
if (isNaN(userId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid user ID' });
|
||||||
|
}
|
||||||
|
|
||||||
const deletedCount = await deleteUserById(userId);
|
const deletedCount = await deleteUserById(userId);
|
||||||
// console.log('[ROUTER] deleteUserById вернул:', deletedCount);
|
|
||||||
if (deletedCount === 0) {
|
if (deletedCount === 0) {
|
||||||
return res.status(404).json({ success: false, deleted: 0, error: 'User not found' });
|
return res.status(404).json({ success: false, deleted: 0, error: 'User not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastContactsUpdate();
|
broadcastContactsUpdate();
|
||||||
res.json({ success: true, deleted: deletedCount });
|
res.json({ success: true, deleted: deletedCount });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error('[DELETE] Ошибка при удалении пользователя:', e);
|
console.error('[DELETE] Ошибка при удалении:', e);
|
||||||
res.status(500).json({ error: 'DB error', details: e.message });
|
res.status(500).json({ error: 'DB error', details: e.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -457,26 +552,36 @@ router.get('/:id', async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const query = db.getQuery();
|
const query = db.getQuery();
|
||||||
|
|
||||||
// Проверяем, это гостевой идентификатор (формат: channel:rawId)
|
// Проверяем, это гостевой идентификатор (формат: guest_123)
|
||||||
if (userId.includes(':')) {
|
if (userId.startsWith('guest_')) {
|
||||||
|
const guestId = parseInt(userId.replace('guest_', ''));
|
||||||
|
|
||||||
|
if (isNaN(guestId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid guest ID format' });
|
||||||
|
}
|
||||||
|
|
||||||
const guestResult = await query(
|
const guestResult = await query(
|
||||||
`WITH decrypted_guest AS (
|
`WITH decrypted_guest AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
id,
|
||||||
decrypt_text(identifier_encrypted, $2) as guest_identifier,
|
decrypt_text(identifier_encrypted, $2) as guest_identifier,
|
||||||
channel,
|
channel,
|
||||||
created_at
|
created_at,
|
||||||
|
user_id
|
||||||
FROM unified_guest_messages
|
FROM unified_guest_messages
|
||||||
WHERE decrypt_text(identifier_encrypted, $2) = $1
|
WHERE user_id IS NULL
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
|
MIN(id) as guest_id,
|
||||||
guest_identifier,
|
guest_identifier,
|
||||||
channel,
|
channel,
|
||||||
MIN(created_at) as created_at,
|
MIN(created_at) as created_at,
|
||||||
MAX(created_at) as last_message_at,
|
MAX(created_at) as last_message_at,
|
||||||
COUNT(*) as message_count
|
COUNT(*) as message_count
|
||||||
FROM decrypted_guest
|
FROM decrypted_guest
|
||||||
GROUP BY guest_identifier, channel`,
|
GROUP BY guest_identifier, channel
|
||||||
[userId, encryptionKey]
|
HAVING MIN(id) = $1`,
|
||||||
|
[guestId, encryptionKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (guestResult.rows.length === 0) {
|
if (guestResult.rows.length === 0) {
|
||||||
@@ -484,7 +589,7 @@ router.get('/:id', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const guest = guestResult.rows[0];
|
const guest = guestResult.rows[0];
|
||||||
const rawId = userId.replace(`${guest.channel}:`, '');
|
const rawId = guest.guest_identifier.replace(`${guest.channel}:`, '');
|
||||||
const channelMap = {
|
const channelMap = {
|
||||||
'web': '🌐',
|
'web': '🌐',
|
||||||
'telegram': '📱',
|
'telegram': '📱',
|
||||||
@@ -492,9 +597,20 @@ router.get('/:id', async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
const icon = channelMap[guest.channel] || '👤';
|
const icon = channelMap[guest.channel] || '👤';
|
||||||
|
|
||||||
|
// Формируем имя в зависимости от канала
|
||||||
|
let displayName;
|
||||||
|
if (guest.channel === 'email') {
|
||||||
|
displayName = `${icon} ${rawId}`;
|
||||||
|
} else if (guest.channel === 'telegram') {
|
||||||
|
displayName = `${icon} Telegram (${rawId})`;
|
||||||
|
} else {
|
||||||
|
displayName = `${icon} Гость ${guestId}`;
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
id: userId,
|
id: `guest_${guestId}`,
|
||||||
name: `${icon} ${guest.channel === 'web' ? 'Гость' : guest.channel} (${rawId.substring(0, 8)}...)`,
|
guest_identifier: guest.guest_identifier,
|
||||||
|
name: displayName,
|
||||||
email: guest.channel === 'email' ? rawId : null,
|
email: guest.channel === 'email' ? rawId : null,
|
||||||
telegram: guest.channel === 'telegram' ? rawId : null,
|
telegram: guest.channel === 'telegram' ? rawId : null,
|
||||||
wallet: null,
|
wallet: null,
|
||||||
|
|||||||
@@ -37,15 +37,33 @@ async function startServer() {
|
|||||||
console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message);
|
console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Инициализация ботов сразу при старте (не ждем Ollama)
|
// ⏳ НОВОЕ: Ожидание готовности Ollama перед запуском ботов
|
||||||
console.log('[Server] ▶️ Импортируем BotManager...');
|
const { waitForOllama } = require('./utils/waitForOllama');
|
||||||
const botManager = require('./services/botManager');
|
|
||||||
console.log('[Server] ▶️ Вызываем botManager.initialize()...');
|
// Запускаем ожидание Ollama в фоне (не блокируем старт сервера)
|
||||||
botManager.initialize()
|
waitForOllama({
|
||||||
|
maxWaitTime: 4 * 60 * 1000, // 4 минуты
|
||||||
|
retryInterval: 5000, // 5 секунд между попытками
|
||||||
|
required: false // Не обязательно - запустим боты даже без Ollama
|
||||||
|
})
|
||||||
|
.then((ollamaReady) => {
|
||||||
|
if (ollamaReady) {
|
||||||
|
console.log('[Server] ✅ Ollama готов к работе');
|
||||||
|
} else {
|
||||||
|
console.warn('[Server] ⚠️ Ollama не готов, боты будут работать с ограниченным функционалом AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация ботов ПОСЛЕ ожидания Ollama
|
||||||
|
console.log('[Server] ▶️ Импортируем BotManager...');
|
||||||
|
const botManager = require('./services/botManager');
|
||||||
|
console.log('[Server] ▶️ Вызываем botManager.initialize()...');
|
||||||
|
|
||||||
|
return botManager.initialize();
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('[Server] ✅ botManager.initialize() завершен');
|
console.log('[Server] ✅ botManager.initialize() завершен');
|
||||||
|
|
||||||
// ✨ НОВОЕ: Запускаем AI Queue Worker после инициализации ботов
|
// ✨ Запускаем AI Queue Worker после инициализации ботов
|
||||||
if (process.env.USE_AI_QUEUE !== 'false') {
|
if (process.env.USE_AI_QUEUE !== 'false') {
|
||||||
const ragService = require('./services/ragService');
|
const ragService = require('./services/ragService');
|
||||||
ragService.startQueueWorker();
|
ragService.startQueueWorker();
|
||||||
@@ -53,7 +71,7 @@ async function startServer() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('[Server] ❌ Ошибка botManager.initialize():', error.message);
|
console.error('[Server] ❌ Ошибка инициализации:', error.message);
|
||||||
logger.error('[Server] Ошибка инициализации ботов:', error);
|
logger.error('[Server] Ошибка инициализации ботов:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const ollamaConfig = require('./ollamaConfig');
|
const ollamaConfig = require('./ollamaConfig');
|
||||||
|
const { shouldProcessWithAI } = require('../utils/languageFilter');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI Assistant - тонкая обёртка для работы с Ollama и RAG
|
* AI Assistant - тонкая обёртка для работы с Ollama и RAG
|
||||||
@@ -70,6 +71,18 @@ class AIAssistant {
|
|||||||
try {
|
try {
|
||||||
logger.info(`[AIAssistant] Генерация ответа для канала ${channel}, пользователь ${userId}`);
|
logger.info(`[AIAssistant] Генерация ответа для канала ${channel}, пользователь ${userId}`);
|
||||||
|
|
||||||
|
// 0. Проверяем язык сообщения (только русский)
|
||||||
|
const languageCheck = shouldProcessWithAI(userQuestion);
|
||||||
|
if (!languageCheck.shouldProcess) {
|
||||||
|
logger.info(`[AIAssistant] ⚠️ Пропуск обработки: ${languageCheck.reason} (user: ${userId}, channel: ${channel})`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: languageCheck.reason,
|
||||||
|
skipped: true,
|
||||||
|
message: 'AI обрабатывает только сообщения на русском языке'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const messageDeduplicationService = require('./messageDeduplicationService');
|
const messageDeduplicationService = require('./messageDeduplicationService');
|
||||||
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
|
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
|
||||||
const aiAssistantRulesService = require('./aiAssistantRulesService');
|
const aiAssistantRulesService = require('./aiAssistantRulesService');
|
||||||
|
|||||||
@@ -57,12 +57,20 @@ class BotManager {
|
|||||||
logger.warn('[BotManager] Telegram Bot не инициализирован:', error.message);
|
logger.warn('[BotManager] Telegram Bot не инициализирован:', error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Устанавливаем централизованный процессор сообщений для Telegram
|
||||||
|
telegramBot.setMessageProcessor(this.processMessage.bind(this));
|
||||||
|
logger.info('[BotManager] ✅ Telegram Bot подключен к unified processor');
|
||||||
|
|
||||||
// Инициализируем Email Bot
|
// Инициализируем Email Bot
|
||||||
logger.info('[BotManager] Инициализация Email Bot...');
|
logger.info('[BotManager] Инициализация Email Bot...');
|
||||||
await emailBot.initialize().catch(error => {
|
await emailBot.initialize().catch(error => {
|
||||||
logger.warn('[BotManager] Email Bot не инициализирован:', error.message);
|
logger.warn('[BotManager] Email Bot не инициализирован:', error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Устанавливаем централизованный процессор сообщений для Email
|
||||||
|
emailBot.setMessageProcessor(this.processMessage.bind(this));
|
||||||
|
logger.info('[BotManager] ✅ Email Bot подключен к unified processor');
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
logger.info('[BotManager] ✅ BotManager успешно инициализирован');
|
logger.info('[BotManager] ✅ BotManager успешно инициализирован');
|
||||||
|
|
||||||
|
|||||||
@@ -150,14 +150,14 @@ async function getEmbeddingModel() {
|
|||||||
function getTimeouts() {
|
function getTimeouts() {
|
||||||
return {
|
return {
|
||||||
// Ollama API - таймауты запросов
|
// Ollama API - таймауты запросов
|
||||||
ollamaChat: 120000, // 120 сек (2 мин) - генерация ответов LLM
|
ollamaChat: 180000, // 180 сек (3 мин) - генерация ответов LLM (увеличено для сложных запросов)
|
||||||
ollamaEmbedding: 60000, // 60 сек (1 мин) - генерация embeddings
|
ollamaEmbedding: 90000, // 90 сек (1.5 мин) - генерация embeddings (увеличено)
|
||||||
ollamaHealth: 5000, // 5 сек - health check
|
ollamaHealth: 5000, // 5 сек - health check
|
||||||
ollamaTags: 10000, // 10 сек - список моделей
|
ollamaTags: 10000, // 10 сек - список моделей
|
||||||
|
|
||||||
// Vector Search - таймауты запросов
|
// Vector Search - таймауты запросов
|
||||||
vectorSearch: 30000, // 30 сек - поиск по векторам
|
vectorSearch: 90000, // 90 сек - поиск по векторам (увеличено для больших баз)
|
||||||
vectorUpsert: 60000, // 60 сек - индексация данных
|
vectorUpsert: 90000, // 90 сек - индексация данных (увеличено)
|
||||||
vectorHealth: 5000, // 5 сек - health check
|
vectorHealth: 5000, // 5 сек - health check
|
||||||
|
|
||||||
// AI Cache - TTL (Time To Live) для кэширования
|
// AI Cache - TTL (Time To Live) для кэширования
|
||||||
@@ -166,12 +166,12 @@ function getTimeouts() {
|
|||||||
cacheMax: 1000, // Максимум записей в кэше
|
cacheMax: 1000, // Максимум записей в кэше
|
||||||
|
|
||||||
// AI Queue - параметры очереди
|
// AI Queue - параметры очереди
|
||||||
queueTimeout: 120000, // 120 сек - таймаут задачи в очереди
|
queueTimeout: 180000, // 180 сек - таймаут задачи в очереди (увеличено)
|
||||||
queueMaxSize: 100, // Максимум задач в очереди
|
queueMaxSize: 100, // Максимум задач в очереди
|
||||||
queueInterval: 100, // 100 мс - интервал проверки очереди
|
queueInterval: 100, // 100 мс - интервал проверки очереди
|
||||||
|
|
||||||
// Default для совместимости
|
// Default для совместимости
|
||||||
default: 120000 // 120 сек
|
default: 180000 // 180 сек (увеличено с 120)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -395,6 +395,15 @@ async function generateLLMResponse({
|
|||||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||||
const timeouts = ollamaConfig.getTimeouts();
|
const timeouts = ollamaConfig.getTimeouts();
|
||||||
|
|
||||||
|
// Логируем размер промпта для отладки
|
||||||
|
const promptSize = JSON.stringify(messages).length;
|
||||||
|
console.log(`[RAG] Отправка запроса в Ollama. Размер промпта: ${promptSize} символов, таймаут: ${timeouts.ollamaChat/1000}с`);
|
||||||
|
|
||||||
|
// Проверяем размер промпта и предупреждаем, если он большой
|
||||||
|
if (promptSize > 10000) {
|
||||||
|
console.warn(`[RAG] ⚠️ Большой промпт (${promptSize} символов). Возможны проблемы с производительностью.`);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios.post(`${ollamaUrl}/api/chat`, {
|
const response = await axios.post(`${ollamaUrl}/api/chat`, {
|
||||||
model: model || ollamaConfig.getDefaultModel(),
|
model: model || ollamaConfig.getDefaultModel(),
|
||||||
messages: messages,
|
messages: messages,
|
||||||
@@ -406,7 +415,17 @@ async function generateLLMResponse({
|
|||||||
llmResponse = response.data.message.content;
|
llmResponse = response.data.message.content;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[RAG] Error in Ollama call:`, error.message);
|
const isTimeout = error.message && (
|
||||||
|
error.message.includes('timeout') ||
|
||||||
|
error.message.includes('ETIMEDOUT') ||
|
||||||
|
error.message.includes('ECONNABORTED')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTimeout) {
|
||||||
|
console.warn(`[RAG] Ollama timeout после ${timeouts.ollamaChat/1000}с. Возможно, модель перегружена или контекст слишком большой.`);
|
||||||
|
} else {
|
||||||
|
console.error(`[RAG] Error in Ollama call:`, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Финальный fallback - возврат ответа из RAG
|
// Финальный fallback - возврат ответа из RAG
|
||||||
if (answer) {
|
if (answer) {
|
||||||
@@ -414,6 +433,11 @@ async function generateLLMResponse({
|
|||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если был таймаут и нет ответа из RAG - возвращаем более информативное сообщение
|
||||||
|
if (isTimeout) {
|
||||||
|
return 'Извините, обработка запроса заняла слишком много времени. Пожалуйста, попробуйте упростить ваш вопрос или повторите попытку позже.';
|
||||||
|
}
|
||||||
|
|
||||||
return 'Извините, произошла ошибка при генерации ответа.';
|
return 'Извините, произошла ошибка при генерации ответа.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
85
backend/utils/languageFilter.js
Normal file
85
backend/utils/languageFilter.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Фильтр сообщений по языку
|
||||||
|
* AI ассистент работает только на русском языке
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет наличие кириллицы в тексте
|
||||||
|
*/
|
||||||
|
function hasCyrillic(text) {
|
||||||
|
if (!text || typeof text !== 'string') return false;
|
||||||
|
return /[а-яА-ЯЁё]/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определяет процент кириллицы в тексте
|
||||||
|
*/
|
||||||
|
function getCyrillicPercentage(text) {
|
||||||
|
if (!text) return 0;
|
||||||
|
const cyrillicChars = (text.match(/[а-яА-ЯЁё]/g) || []).length;
|
||||||
|
const totalChars = text.replace(/\s/g, '').length;
|
||||||
|
return totalChars > 0 ? (cyrillicChars / totalChars) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, является ли сообщение на русском языке
|
||||||
|
* @param {string} message - текст сообщения
|
||||||
|
* @param {number} minCyrillicPercent - минимальный % кириллицы (по умолчанию 10%)
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isRussianMessage(message, minCyrillicPercent = 10) {
|
||||||
|
if (!message || typeof message !== 'string') return false;
|
||||||
|
|
||||||
|
// Убираем пробелы и спецсимволы для точного подсчета
|
||||||
|
const cleanText = message.trim();
|
||||||
|
|
||||||
|
// Если сообщение очень короткое (например "Hi"), считаем русским
|
||||||
|
if (cleanText.length < 10) {
|
||||||
|
return hasCyrillic(cleanText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для длинных сообщений проверяем процент кириллицы
|
||||||
|
const cyrillicPercent = getCyrillicPercentage(cleanText);
|
||||||
|
|
||||||
|
return cyrillicPercent >= minCyrillicPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определяет, нужно ли обрабатывать сообщение AI
|
||||||
|
* @param {string} message - текст сообщения
|
||||||
|
* @returns {Object} { shouldProcess: boolean, reason: string }
|
||||||
|
*/
|
||||||
|
function shouldProcessWithAI(message) {
|
||||||
|
if (!message || typeof message !== 'string') {
|
||||||
|
return { shouldProcess: false, reason: 'Empty message' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanMessage = message.trim();
|
||||||
|
|
||||||
|
// Проверка на русский язык
|
||||||
|
if (!isRussianMessage(cleanMessage)) {
|
||||||
|
return {
|
||||||
|
shouldProcess: false,
|
||||||
|
reason: 'Non-Russian message (AI works only with Russian)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка на максимальный размер (опционально)
|
||||||
|
const MAX_LENGTH = 10000;
|
||||||
|
if (cleanMessage.length > MAX_LENGTH) {
|
||||||
|
return {
|
||||||
|
shouldProcess: false,
|
||||||
|
reason: `Message too long (${cleanMessage.length} > ${MAX_LENGTH} chars)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { shouldProcess: true, reason: 'OK' };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hasCyrillic,
|
||||||
|
getCyrillicPercentage,
|
||||||
|
isRussianMessage,
|
||||||
|
shouldProcessWithAI
|
||||||
|
};
|
||||||
|
|
||||||
148
backend/utils/waitForOllama.js
Normal file
148
backend/utils/waitForOllama.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
const logger = require('./logger');
|
||||||
|
const ollamaConfig = require('../services/ollamaConfig');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, загружена ли модель в память через /api/ps
|
||||||
|
* НЕ триггерит загрузку модели (в отличие от /api/generate)
|
||||||
|
* @param {string} modelName - название модели (например: qwen2.5:7b)
|
||||||
|
* @returns {Promise<boolean>} true если модель в памяти
|
||||||
|
*/
|
||||||
|
async function isModelLoaded(modelName) {
|
||||||
|
try {
|
||||||
|
const axios = require('axios');
|
||||||
|
const baseUrl = ollamaConfig.getBaseUrl();
|
||||||
|
|
||||||
|
// Используем /api/ps - показывает какие модели сейчас в памяти
|
||||||
|
// Этот endpoint НЕ триггерит загрузку модели!
|
||||||
|
const response = await axios.get(`${baseUrl}/api/ps`, {
|
||||||
|
timeout: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Проверяем, есть ли наша модель в списке загруженных
|
||||||
|
if (response.data && response.data.models) {
|
||||||
|
return response.data.models.some(m => {
|
||||||
|
// Сравниваем без тега (qwen2.5 == qwen2.5:7b)
|
||||||
|
const modelBaseName = modelName.split(':')[0];
|
||||||
|
const loadedBaseName = (m.name || m.model || '').split(':')[0];
|
||||||
|
return loadedBaseName === modelBaseName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
// API ps может не существовать в старых версиях Ollama
|
||||||
|
// Или модель не загружена
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ожидание готовности Ollama и загрузки модели в память
|
||||||
|
* Ollama может загружаться до 4 минут при старте Docker контейнера
|
||||||
|
* entrypoint.sh загружает модель qwen2.5:7b в память с keep_alive=24h
|
||||||
|
*
|
||||||
|
* @param {Object} options - Опции ожидания
|
||||||
|
* @param {number} options.maxWaitTime - Максимальное время ожидания в мс (по умолчанию 4 минуты)
|
||||||
|
* @param {number} options.retryInterval - Интервал между попытками в мс (по умолчанию 5 секунд)
|
||||||
|
* @param {boolean} options.required - Обязательно ли ждать Ollama (по умолчанию false)
|
||||||
|
* @returns {Promise<boolean>} true если Ollama готов, false если таймаут (и required=false)
|
||||||
|
*/
|
||||||
|
async function waitForOllama(options = {}) {
|
||||||
|
const {
|
||||||
|
maxWaitTime = parseInt(process.env.OLLAMA_WAIT_TIME) || 4 * 60 * 1000,
|
||||||
|
retryInterval = parseInt(process.env.OLLAMA_RETRY_INTERVAL) || 5000,
|
||||||
|
required = process.env.OLLAMA_REQUIRED === 'true' || false
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
let attempt = 0;
|
||||||
|
const maxAttempts = Math.ceil(maxWaitTime / retryInterval);
|
||||||
|
|
||||||
|
logger.info(`[waitForOllama] ⏳ Ожидание готовности Ollama и загрузки модели в память (макс. ${maxWaitTime/1000}с, интервал ${retryInterval/1000}с)...`);
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxWaitTime) {
|
||||||
|
attempt++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Шаг 1: Проверяем доступность Ollama API
|
||||||
|
const healthStatus = await ollamaConfig.checkHealth();
|
||||||
|
|
||||||
|
if (healthStatus.status === 'ok') {
|
||||||
|
const model = healthStatus.model;
|
||||||
|
|
||||||
|
// Шаг 2: Проверяем, загружена ли модель в память
|
||||||
|
const modelReady = await isModelLoaded(model);
|
||||||
|
|
||||||
|
if (modelReady) {
|
||||||
|
const waitedTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
|
logger.info(`[waitForOllama] ✅ Ollama готов! Модель ${model} загружена в память (ожидание ${waitedTime}с, попытка ${attempt}/${maxAttempts})`);
|
||||||
|
logger.info(`[waitForOllama] 📊 Ollama: ${healthStatus.baseUrl}, доступно моделей: ${healthStatus.availableModels}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.info(`[waitForOllama] ⏳ Ollama API готов, но модель ${model} ещё грузится в память... (попытка ${attempt}/${maxAttempts})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[waitForOllama] ⚠️ Ollama API не готов (попытка ${attempt}/${maxAttempts}): ${healthStatus.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[waitForOllama] ⚠️ Ошибка проверки Ollama (попытка ${attempt}/${maxAttempts}): ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не последняя попытка - ждем перед следующей
|
||||||
|
if (Date.now() - startTime < maxWaitTime) {
|
||||||
|
const remainingTime = Math.max(0, maxWaitTime - (Date.now() - startTime));
|
||||||
|
const nextRetry = Math.min(retryInterval, remainingTime);
|
||||||
|
|
||||||
|
if (nextRetry > 0) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, nextRetry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Таймаут истек
|
||||||
|
const totalWaitTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
|
|
||||||
|
if (required) {
|
||||||
|
const error = `Ollama не готов после ${totalWaitTime}с ожидания (${attempt} попыток)`;
|
||||||
|
logger.error(`[waitForOllama] ❌ ${error}`);
|
||||||
|
throw new Error(error);
|
||||||
|
} else {
|
||||||
|
logger.warn(`[waitForOllama] ⚠️ Ollama не готов после ${totalWaitTime}с ожидания (${attempt} попыток). Продолжаем без AI.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка готовности Ollama (одна попытка, без ожидания)
|
||||||
|
* @returns {Promise<boolean>} true если Ollama готов
|
||||||
|
*/
|
||||||
|
async function isOllamaReady() {
|
||||||
|
try {
|
||||||
|
const healthStatus = await ollamaConfig.checkHealth();
|
||||||
|
if (healthStatus.status !== 'ok') return false;
|
||||||
|
|
||||||
|
// Проверяем модель
|
||||||
|
return await isModelLoaded(healthStatus.model);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
waitForOllama,
|
||||||
|
isOllamaReady
|
||||||
|
};
|
||||||
|
|
||||||
@@ -379,21 +379,39 @@ function formatDate(date) {
|
|||||||
}
|
}
|
||||||
async function loadMessages() {
|
async function loadMessages() {
|
||||||
if (!contact.value || !contact.value.id) return;
|
if (!contact.value || !contact.value.id) return;
|
||||||
|
|
||||||
|
console.log('[ContactDetailsView] 📥 loadMessages START for:', contact.value.id);
|
||||||
isLoadingMessages.value = true;
|
isLoadingMessages.value = true;
|
||||||
try {
|
try {
|
||||||
// Загружаем ВСЕ публичные сообщения этого пользователя (как на главной странице)
|
// Загружаем ВСЕ публичные сообщения этого пользователя (как на главной странице)
|
||||||
messages.value = await messagesService.getMessagesByUserId(contact.value.id);
|
const loadedMessages = await messagesService.getMessagesByUserId(contact.value.id);
|
||||||
|
console.log('[ContactDetailsView] 📩 Loaded messages:', loadedMessages.length, 'for', contact.value.id);
|
||||||
|
|
||||||
|
messages.value = loadedMessages;
|
||||||
|
|
||||||
if (messages.value.length > 0) {
|
if (messages.value.length > 0) {
|
||||||
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
||||||
} else {
|
} else {
|
||||||
lastMessageDate.value = null;
|
lastMessageDate.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Также получаем conversationId для отправки новых сообщений
|
// Получаем conversationId только для зарегистрированных пользователей
|
||||||
const conv = await messagesService.getConversationByUserId(contact.value.id);
|
// Гости не имеют conversations
|
||||||
conversationId.value = conv?.id || null;
|
if (!contact.value.id.startsWith('guest_')) {
|
||||||
|
try {
|
||||||
|
const conv = await messagesService.getConversationByUserId(contact.value.id);
|
||||||
|
conversationId.value = conv?.id || null;
|
||||||
|
} catch (convError) {
|
||||||
|
console.warn('[ContactDetailsView] Не удалось загрузить conversationId:', convError.message);
|
||||||
|
conversationId.value = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conversationId.value = null; // Гости не имеют conversationId
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ContactDetailsView] ✅ loadMessages DONE, messages count:', messages.value.length);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ContactDetailsView] Ошибка загрузки сообщений:', e);
|
console.error('[ContactDetailsView] ❌ Ошибка загрузки сообщений:', e);
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
lastMessageDate.value = null;
|
lastMessageDate.value = null;
|
||||||
conversationId.value = null;
|
conversationId.value = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user