feat: новая функция

This commit is contained in:
2025-10-23 21:44:14 +03:00
parent 918da882d2
commit 6e21887c3b
17 changed files with 959 additions and 462 deletions

View File

@@ -449,33 +449,36 @@ class UniversalGuestService {
await db.getQuery()(
`INSERT INTO messages (
user_id,
conversation_id,
sender_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
attachment_filename_encrypted,
attachment_mimetype_encrypted,
attachment_filename,
attachment_mimetype,
attachment_size,
attachment_data,
message_type,
user_id,
role,
direction,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $14),
encrypt_text($4, $14),
encrypt_text($5, $14),
encrypt_text($6, $14),
encrypt_text($7, $14),
encrypt_text($8, $14),
encrypt_text($9, $14),
$10, $11, $12, $13
encrypt_text($3, $17),
encrypt_text($4, $17),
encrypt_text($5, $17),
encrypt_text($6, $17),
encrypt_text($7, $17),
$8, $9, $10, $11,
$12, $13, $14, $15,
$16
)`,
[
userId,
conversationId,
userId, // sender_id
senderType,
msg.content,
msg.channel,
@@ -485,7 +488,10 @@ class UniversalGuestService {
msg.attachment_mimetype,
msg.attachment_size,
msg.attachment_data,
'public', // message_type для мигрированных сообщений
'user_chat', // message_type для мигрированных сообщений (личный чат с ИИ)
userId, // user_id
role, // role (незашифрованное)
direction, // direction (незашифрованное)
msg.created_at,
encryptionKey
]

View File

@@ -29,13 +29,14 @@ const logger = require('../utils/logger');
function shouldGenerateAiReply(params) {
const { senderType, userId, recipientId } = params;
// Обычные пользователи (USER, READONLY) всегда получают AI ответ
if (senderType !== 'editor') {
// Если recipientId не указан или равен userId - это личный чат с ИИ
// ИИ должен отвечать в личных чатах
if (!recipientId || recipientId === userId) {
return true;
}
// Админы-редакторы (EDITOR) НЕ получают AI ответы
// ни себе, ни другим админам (по спецификации)
// Если recipientId отличается от userId - это публичный чат между пользователями
// ИИ НЕ должен отвечать на сообщения между пользователями
return false;
}

View File

@@ -30,12 +30,12 @@ async function getOrCreateConversation(userId, title = 'Новая беседа'
// Ищем существующую активную беседу
const { rows: existing } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
`SELECT id, user_id, title, created_at, updated_at
FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC
LIMIT 1`,
[userId, encryptionKey]
[userId]
);
if (existing.length > 0) {
@@ -44,10 +44,10 @@ async function getOrCreateConversation(userId, title = 'Новая беседа'
// Создаем новую беседу
const { rows: newConv } = await db.getQuery()(
`INSERT INTO conversations (user_id, title_encrypted)
VALUES ($1, encrypt_text($2, $3))
RETURNING id, user_id, decrypt_text(title_encrypted, $3) as title, created_at, updated_at`,
[userId, title, encryptionKey]
`INSERT INTO conversations (user_id, title)
VALUES ($1, $2)
RETURNING id, user_id, title, created_at, updated_at`,
[userId, title]
);
logger.info('[ConversationService] Создана новая беседа:', newConv[0].id);
@@ -59,6 +59,60 @@ async function getOrCreateConversation(userId, title = 'Новая беседа'
}
}
/**
* Получить или создать публичную беседу между двумя пользователями
* @param {number} userId1 - ID первого пользователя
* @param {number} userId2 - ID второго пользователя
* @returns {Promise<Object>}
*/
async function getOrCreatePublicConversation(userId1, userId2) {
try {
// Ищем существующую публичную беседу между этими пользователями
const { rows: existing } = await db.getQuery()(
`SELECT c.id, c.user_id, c.title, c.created_at, c.updated_at, c.conversation_type
FROM conversations c
INNER JOIN conversation_participants cp1 ON c.id = cp1.conversation_id
INNER JOIN conversation_participants cp2 ON c.id = cp2.conversation_id
WHERE c.conversation_type = 'public_chat'
AND cp1.user_id = $1 AND cp2.user_id = $2
ORDER BY c.created_at DESC
LIMIT 1`,
[userId1, userId2]
);
if (existing.length > 0) {
return existing[0];
}
// Создаем новую публичную беседу
const { rows: newConv } = await db.getQuery()(
`INSERT INTO conversations (user_id, title, conversation_type)
VALUES ($1, $2, 'public_chat')
RETURNING id, user_id, title, created_at, updated_at, conversation_type`,
[userId1, `Публичная беседа ${userId1}-${userId2}`]
);
const conversation = newConv[0];
// Добавляем участников
await db.getQuery()(
`INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2)`,
[conversation.id, userId1]
);
await db.getQuery()(
`INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2)`,
[conversation.id, userId2]
);
logger.info('[ConversationService] Создана публичная беседа:', conversation.id);
return conversation;
} catch (error) {
logger.error('[ConversationService] Ошибка создания публичной беседы:', error);
throw error;
}
}
/**
* Получить беседу по ID
* @param {number} conversationId - ID беседы
@@ -69,10 +123,10 @@ async function getConversationById(conversationId) {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
`SELECT id, user_id, title, created_at, updated_at
FROM conversations
WHERE id = $1`,
[conversationId, encryptionKey]
[conversationId]
);
return rows.length > 0 ? rows[0] : null;
@@ -93,11 +147,11 @@ async function getUserConversations(userId) {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
`SELECT id, user_id, title, created_at, updated_at
FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC`,
[userId, encryptionKey]
[userId]
);
return rows;
@@ -164,10 +218,10 @@ async function updateConversationTitle(conversationId, userId, newTitle) {
const { rows } = await db.getQuery()(
`UPDATE conversations
SET title_encrypted = encrypt_text($3, $4), updated_at = NOW()
SET title = $3, updated_at = NOW()
WHERE id = $1 AND user_id = $2
RETURNING id, user_id, decrypt_text(title_encrypted, $4) as title, created_at, updated_at`,
[conversationId, userId, newTitle, encryptionKey]
RETURNING id, user_id, title, created_at, updated_at`,
[conversationId, userId, newTitle]
);
return rows.length > 0 ? rows[0] : null;
@@ -180,6 +234,7 @@ async function updateConversationTitle(conversationId, userId, newTitle) {
module.exports = {
getOrCreateConversation,
getOrCreatePublicConversation,
getConversationById,
getUserConversations,
touchConversation,

View File

@@ -18,6 +18,60 @@ const conversationService = require('./conversationService');
const adminLogicService = require('./adminLogicService');
const universalGuestService = require('./UniversalGuestService');
const identityService = require('./identity-service');
/**
* Определить тип сообщения по контексту
* @param {number|null} recipientId - ID получателя
* @param {number} userId - ID отправителя
* @param {boolean} isAdminSender - Является ли отправитель админом
* @returns {string} - Тип сообщения: 'user_chat', 'admin_chat', 'public'
*/
function determineMessageType(recipientId, userId, isAdminSender) {
// 1. Личный чат с ИИ (recipientId не указан или равен userId)
if (!recipientId || recipientId === userId) {
return 'user_chat';
}
// 2. Приватное сообщение к редактору (recipientId = 1)
if (recipientId === 1) {
return 'admin_chat';
}
// 3. Публичное сообщение между пользователями
return 'public';
}
/**
* Определить тип беседы
* @param {string} messageType - Тип сообщения
* @param {number|null} recipientId - ID получателя
* @param {number} userId - ID отправителя
* @returns {string} - Тип беседы: 'user_chat', 'private', 'public'
*/
function determineConversationType(messageType, recipientId, userId) {
switch (messageType) {
case 'user_chat':
return 'user_chat'; // Личная беседа с ИИ
case 'admin_chat':
return 'private'; // Приватная беседа с редактором
case 'public':
return 'public_chat'; // Публичная беседа между пользователями
default:
return 'user_chat';
}
}
/**
* Определить, нужно ли генерировать AI ответ
* @param {string} messageType - Тип сообщения
* @param {number|null} recipientId - ID получателя
* @param {number} userId - ID отправителя
* @returns {boolean}
*/
function shouldGenerateAiReply(messageType, recipientId, userId) {
// ИИ отвечает только в личных чатах
return messageType === 'user_chat';
}
const { broadcastMessagesUpdate } = require('../wsHub');
// НОВАЯ СИСТЕМА РОЛЕЙ: используем shared/permissions.js
const { hasPermission, ROLES, PERMISSIONS } = require('/app/shared/permissions');
@@ -92,24 +146,39 @@ async function processMessage(messageData) {
// НОВАЯ СИСТЕМА РОЛЕЙ: определяем права через новую систему
const isAdmin = userRole === ROLES.EDITOR || userRole === ROLES.READONLY;
// 4. Определяем нужно ли генерировать AI ответ
const shouldGenerateAi = adminLogicService.shouldGenerateAiReply({
senderType: isAdmin ? 'editor' : 'user',
userId: userId,
recipientId: recipientId || userId,
channel: channel
});
// 4. Определяем тип сообщения по контексту
const messageType = determineMessageType(recipientId, userId, isAdmin);
// 5. Определяем нужно ли генерировать AI ответ
const shouldGenerateAi = shouldGenerateAiReply(messageType, recipientId, userId);
logger.info('[UnifiedMessageProcessor] Генерация AI:', { shouldGenerateAi, userRole, isAdmin });
// 5. Получаем или создаем беседу
// 6. Получаем или создаем беседу с правильным типом
let conversation;
const conversationType = determineConversationType(messageType, recipientId, userId);
if (inputConversationId) {
conversation = await conversationService.getConversationById(inputConversationId);
}
if (!conversation) {
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
// Для публичных сообщений создаем беседу между пользователями
if (messageType === 'public') {
conversation = await conversationService.getOrCreatePublicConversation(userId, recipientId);
} else {
// Для личных и админских чатов используем стандартную логику
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
}
// Обновляем тип беседы в БД, если он не соответствует
if (conversation.conversation_type !== conversationType) {
await db.getQuery()(
'UPDATE conversations SET conversation_type = $1 WHERE id = $2',
[conversationType, conversation.id]
);
conversation.conversation_type = conversationType;
}
}
const conversationId = conversation.id;
@@ -133,34 +202,38 @@ async function processMessage(messageData) {
const { rows } = await db.getQuery()(
`INSERT INTO messages (
user_id,
conversation_id,
sender_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
attachment_filename_encrypted,
attachment_mimetype_encrypted,
attachment_filename,
attachment_mimetype,
attachment_size,
attachment_data,
message_type,
user_id,
role,
direction,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $13),
encrypt_text($4, $13),
encrypt_text($5, $13),
encrypt_text($6, $13),
encrypt_text($7, $13),
encrypt_text($8, $13),
encrypt_text($9, $13),
encrypt_text($3, $16),
encrypt_text($4, $16),
encrypt_text($5, $16),
encrypt_text($6, $16),
encrypt_text($7, $16),
$8,
$9,
$10, $11, $12,
$13, $14, $15,
NOW()
) RETURNING id`,
[
userId,
conversationId,
userId, // sender_id
isAdmin ? 'editor' : 'user',
content,
channel,
@@ -170,7 +243,10 @@ async function processMessage(messageData) {
attachment_mimetype,
attachment_size,
attachment_data,
'user_chat', // message_type
messageType, // message_type
recipientId || userId, // user_id (получатель для публичных сообщений)
'user', // role (незашифрованное)
'incoming', // direction (незашифрованное)
encryptionKey
]
);
@@ -220,34 +296,40 @@ async function processMessage(messageData) {
// Сохраняем ответ AI
const { rows: aiMessageRows } = await db.getQuery()(
`INSERT INTO messages (
user_id,
conversation_id,
sender_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
message_type,
user_id,
role,
direction,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $9),
encrypt_text($4, $9),
encrypt_text($5, $9),
encrypt_text($6, $9),
encrypt_text($7, $9),
$8,
encrypt_text($3, $12),
encrypt_text($4, $12),
encrypt_text($5, $12),
encrypt_text($6, $12),
encrypt_text($7, $12),
$8, $9, $10, $11,
NOW()
) RETURNING id`,
[
userId,
conversationId,
userId, // sender_id
'assistant',
aiResponse.response,
channel,
'assistant',
'outgoing',
'user_chat',
messageType,
userId, // user_id
'assistant', // role (незашифрованное)
'outgoing', // direction (незашифрованное)
encryptionKey
]
);

View File

@@ -89,13 +89,7 @@ async function deleteUserById(userId) {
);
console.log('[DELETE] Удалено user_tag_links:', resTagLinks.rows.length);
// 9. Удаляем global_read_status
console.log('[DELETE] Начинаем удаление global_read_status для userId:', userId);
const resReadStatus = await db.getQuery()(
'DELETE FROM global_read_status WHERE user_id = $1 RETURNING user_id',
[userId]
);
console.log('[DELETE] Удалено global_read_status:', resReadStatus.rows.length);
// 9. global_read_status - таблица не существует, пропускаем
// 10. Удаляем самого пользователя
console.log('[DELETE] Начинаем удаление пользователя из users:', userId);