diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts new file mode 100644 index 0000000..b89f8d5 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts @@ -0,0 +1,78 @@ +import { z } from "zod"; +import gigachat from './gigachat'; + +export interface ModerationResult { + comment: string; + isApproved: boolean; + success: boolean; + error?: string; +} + +export class ChatModerationAgent { + private moderationLlm: any; + + constructor(GIGA_AUTH) { + // Создаем структурированный вывод для модерации + this.moderationLlm = gigachat(GIGA_AUTH).withStructuredOutput(z.object({ + comment: z.string(), + isApproved: z.boolean(), + }) as any); + } + + private getSystemPrompt(): string { + return `Ты модерируешь сообщения в чате. Твоя задача - проверить сообщение на нецензурную лексику, брань и неприемлемый контент. + +Твои задачи: +1. Проверь сообщение на наличие нецензурной лексики, мата, ругательств и брани. +2. Проверь на оскорбления, угрозы и агрессивное поведение. +3. Проверь на спам и рекламу. +4. Проверь на неприемлемый контент (дискриминация, экстремизм и т.д.). + +- Если сообщение не содержит запрещенного контента, оно одобряется (isApproved: true). +- Если сообщение содержит запрещенный контент, оно отклоняется (isApproved: false). + +Правила написания комментария: +- Если сообщение одобряется, оставь поле comment пустым. +- Если сообщение отклоняется, пиши комментарий со следующей формулировкой: + "Сообщение удалено. Причина: (укажи конкретную причину: нецензурная лексика, оскорбления, спам и т.д.)"`; + } + + public async moderateMessage(message: string): Promise { + try { + const prompt = `${this.getSystemPrompt()} + +Сообщение: ${message}`; + + const result = await this.moderationLlm.invoke(prompt); + + // Дополнительная проверка + if (!result.isApproved && result.comment.trim() === '') { + result.comment = 'Сообщение удалено. Причина: нарушение правил чата.'; + } + + return { + comment: result.comment, + isApproved: result.isApproved, + success: true + }; + + } catch (error) { + console.error('❌ [Chat Moderation] Ошибка при модерации:', error); + + // В случае ошибки одобряем сообщение + return { + comment: '', + isApproved: true, + success: false, + error: error instanceof Error ? error.message : 'Неизвестная ошибка' + }; + } + } +} + +// Экспортируем функцию для обратной совместимости +export const moderationText = async (title: string, body: string, GIGA_AUTH): Promise<[string, boolean, string]> => { + const agent = new ChatModerationAgent(GIGA_AUTH); + const result = await agent.moderateMessage(body); + return [result.comment, result.isApproved, body]; +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts new file mode 100644 index 0000000..609020a --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts @@ -0,0 +1,18 @@ +import { Agent } from 'node:https'; +import { GigaChat } from 'langchain-gigachat'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js) +export const gigachat = (GIGA_AUTH) => new + GigaChat({ + model: 'GigaChat-2', + scope: 'GIGACHAT_API_PERS', + streaming: false, + credentials: GIGA_AUTH, + httpsAgent +}); + +export default gigachat; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js new file mode 100644 index 0000000..c4efec6 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js @@ -0,0 +1,16 @@ +// Конфигурация системы модерации +const MODERATION_CONFIG = { + // Задержка перед запуском модерации (в миллисекундах) + MODERATION_DELAY: 1500, // 1.5 секунды + + // Включена ли система модерации + MODERATION_ENABLED: true, + + // Текст для замены заблокированных сообщений + BLOCKED_MESSAGE_TEXT: '[Удалено модератором]', + + // Логировать ли процесс модерации + ENABLE_MODERATION_LOGS: true +}; + +module.exports = MODERATION_CONFIG; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 2fdc6bd..653579f 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,4 +1,6 @@ const router = require('express').Router(); + + const authRouter = require('./auth'); const { supabaseRouter } = require('./supabaseClient'); const profileRouter = require('./profile'); @@ -8,7 +10,11 @@ const additionalServicesRouter = require('./additional_services'); const chatsRouter = require('./chats'); const camerasRouter = require('./cameras'); const ticketsRouter = require('./tickets'); + const messagesRouter = require('./messages'); + +const moderationRouter = require('./moderation'); + const utilityPaymentsRouter = require('./utility_payments'); const apartmentsRouter = require('./apartments'); const buildingsRouter = require('./buildings'); @@ -19,6 +25,7 @@ const supportRouter = require('./supportApi'); module.exports = router; + router.use('/auth', authRouter); router.use('/supabase', supabaseRouter); router.use('', profileRouter); @@ -28,7 +35,11 @@ router.use('', additionalServicesRouter); router.use('', chatsRouter); router.use('', camerasRouter); router.use('', ticketsRouter); + router.use('', messagesRouter); + +router.use('', moderationRouter); + router.use('', utilityPaymentsRouter); router.use('', apartmentsRouter); router.use('', buildingsRouter); @@ -38,3 +49,4 @@ router.use('', supportRouter); + diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js index f729679..a495e33 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,6 +1,20 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -const { getIo } = require('../../../io'); // Импортируем Socket.IO +const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Импортируем функцию модерации +const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); // Импортируем конфигурацию модерации + + +// Добавляем middleware для логирования всех запросов к messages роутеру + +// Тестовый эндпоинт для проверки работы роутера +router.get('/messages/test', (req, res) => { + res.json({ + status: 'OK', + message: 'Messages router работает', + timestamp: new Date().toISOString(), + moderation_config: MODERATION_CONFIG + }); +}); // Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { @@ -59,10 +73,21 @@ router.get('/messages', async (req, res) => { // Создать новое сообщение router.post('/messages', async (req, res) => { - const supabase = getSupabaseClient(); + + let supabase; + try { + supabase = getSupabaseClient(); + } catch (error) { + console.error(`❌ [Message Send] Ошибка получения Supabase клиента:`, error); + return res.status(500).json({ error: 'Database connection error' }); + } + const { chat_id, user_id, text } = req.body; + if (!chat_id || !user_id || !text) { + console.log(`❌ [Message Send] Отклонен: отсутствуют обязательные поля`); + console.log(`❌ [Message Send] chat_id: ${chat_id}, user_id: ${user_id}, text: ${text}`); return res.status(400).json({ error: 'chat_id, user_id, and text are required' }); @@ -75,23 +100,27 @@ router.post('/messages', async (req, res) => { .select('*') .single(); - if (error) return res.status(400).json({ error: error.message }); + if (error) { + console.error(`❌ [Message Send] Ошибка сохранения в Supabase:`, error); + return res.status(400).json({ error: error.message }); + } // Получаем профиль пользователя - const { data: userProfile } = await supabase + const { data: userProfile, error: profileError } = await supabase .from('user_profiles') .select('id, full_name, avatar_url') .eq('id', user_id) .single(); + + if (profileError) { + console.log(`⚠️ [Message Send] Профиль пользователя не найден:`, profileError); + } // Объединяем сообщение с профилем const data = { ...newMessage, user_profiles: userProfile || null }; - - // Отправка через Socket.IO теперь происходит автоматически через Supabase Real-time подписку - // Это предотвращает дублирование сообщений res.json(data); }); @@ -203,6 +232,4 @@ router.delete('/messages/:message_id', async (req, res) => { res.json({ success: true, message: 'Message deleted successfully' }); }); - - module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/moderation.js new file mode 100644 index 0000000..7f54af9 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/moderation.js @@ -0,0 +1,53 @@ +const router = require('express').Router(); +const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); +const { moderationText } = require('./chat-ai-agent/chat-moderation'); + +// Получить текущие настройки модерации +router.get('/moderation/config', (req, res) => { + res.json(MODERATION_CONFIG); +}); + +// Обновить настройки модерации +router.post('/moderation/config', (req, res) => { + + const oldConfig = { ...MODERATION_CONFIG }; + const { MODERATION_DELAY, MODERATION_ENABLED, BLOCKED_MESSAGE_TEXT, ENABLE_MODERATION_LOGS } = req.body; + + const changes = []; + + if (MODERATION_DELAY !== undefined) { + const newValue = parseInt(MODERATION_DELAY); + MODERATION_CONFIG.MODERATION_DELAY = newValue; + changes.push(`MODERATION_DELAY: ${oldConfig.MODERATION_DELAY} -> ${newValue}`); + } + if (MODERATION_ENABLED !== undefined) { + const newValue = Boolean(MODERATION_ENABLED); + MODERATION_CONFIG.MODERATION_ENABLED = newValue; + changes.push(`MODERATION_ENABLED: ${oldConfig.MODERATION_ENABLED} -> ${newValue}`); + } + if (BLOCKED_MESSAGE_TEXT !== undefined) { + const newValue = String(BLOCKED_MESSAGE_TEXT); + MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT = newValue; + changes.push(`BLOCKED_MESSAGE_TEXT: "${oldConfig.BLOCKED_MESSAGE_TEXT}" -> "${newValue}"`); + } + if (ENABLE_MODERATION_LOGS !== undefined) { + const newValue = Boolean(ENABLE_MODERATION_LOGS) + MODERATION_CONFIG.ENABLE_MODERATION_LOGS = newValue; + changes.push(`ENABLE_MODERATION_LOGS: ${oldConfig.ENABLE_MODERATION_LOGS} -> ${newValue}`); + } + + if (changes.length > 0) { + changes.forEach((change, index) => { + }); + } else { + } + + res.json({ + success: true, + message: 'Настройки модерации обновлены', + changes: changes, + config: MODERATION_CONFIG + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js index db528ca..d33ead2 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js +++ b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js @@ -1,4 +1,12 @@ const { getSupabaseClient, initializationPromise } = require('./supabaseClient'); +const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); +const { getGigaAuth } = require('./get-constants'); +const { moderationText } = require('./chat-ai-agent/chat-moderation'); + +async function getGigaKey() { + const GIGA_AUTH = await getGigaAuth(); + return GIGA_AUTH; +} class ChatPollingHandler { constructor() { @@ -146,6 +154,16 @@ class ChatPollingHandler { const lastEventId = parseInt(last_event_id) || 0; const newEvents = eventQueue.filter(event => event.id > lastEventId); + // Логируем отправку событий клиенту + if (newEvents.length > 0) { + console.log(`📨 [Polling Server] Отправляем ${newEvents.length} событий клиенту ${user_id}`); + newEvents.forEach(event => { + if (event.event === 'message_updated') { + console.log(`📨 [Polling Server] → Событие: ${event.event}, Сообщение ID: ${event.data?.message?.id}, Текст: "${event.data?.message?.text?.substring(0, 50)}${(event.data?.message?.text?.length || 0) > 50 ? '...' : ''}"`); + } + }); + } + res.json({ success: true, events: newEvents, @@ -507,8 +525,8 @@ class ChatPollingHandler { // Очистка старых событий cleanupOldEvents() { const now = new Date(); - const MAX_EVENT_AGE = 24 * 60 * 60 * 1000; // 24 часа - const INACTIVE_USER_THRESHOLD = 60 * 60 * 1000; // 1 час + const MAX_EVENT_AGE = 1 * 60 * 60 * 1000; // 1 час + const INACTIVE_USER_THRESHOLD = 30 * 60 * 1000; // 30 минут // Очищаем старые события this.userEventQueues.forEach((eventQueue, user_id) => { @@ -614,7 +632,7 @@ class ChatPollingHandler { if (profileError) { console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); - } + } // Объединяем сообщение с профилем const messageWithProfile = { @@ -622,14 +640,77 @@ class ChatPollingHandler { user_profiles: userProfile || null }; - // Отправляем сообщение всем участникам чата + // Отправляем сообщение всем участникам чат this.broadcastToChat(newMessage.chat_id, 'new_message', { message: messageWithProfile, timestamp: new Date() }); + + // === ЗАПУСК МОДЕРАЦИИ === + if (MODERATION_CONFIG.MODERATION_ENABLED) { + + if (MODERATION_CONFIG.MODERATION_DELAY === 0) { + setImmediate(() => { + this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); + }); + } else { + const timeoutId = setTimeout(() => { + this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); + }, MODERATION_CONFIG.MODERATION_DELAY); + + } + + } + + } catch (callbackError) { + console.error('❌ [Supabase] Ошибка в обработчике сообщения:', callbackError); + console.error('❌ [Supabase] Stack trace:', callbackError.stack); + } + } + ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const updatedMessage = payload.new; + if (!updatedMessage) { + return; + } + + if (!updatedMessage.chat_id) { + return; + } + + // Получаем профиль пользователя + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', updatedMessage.user_id) + .single(); + + if (profileError) { + console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); + } + + // Объединяем сообщение с профилем + const messageWithProfile = { + ...updatedMessage, + user_profiles: userProfile || null + }; + + // Отправляем обновление всем участникам чат + this.broadcastToChat(updatedMessage.chat_id, 'message_updated', { + message: messageWithProfile, + timestamp: new Date() + }); } catch (callbackError) { - console.error('❌ [Supabase] Ошибка в обработчике сообщения:', callbackError); + console.error('❌ [Supabase] Ошибка в обработчике обновления сообщения:', callbackError); } } ) @@ -654,6 +735,85 @@ class ChatPollingHandler { } } + // Функция отложенной модерации сообщения + async moderateMessage(messageId, messageText, chatId) { + const moderationStartTime = Date.now(); + + try { + + // Вызываем функцию модерации + + let comment, isApproved, finalMessage; + const GIGA_AUTH = await getGigaKey(); + console.log(GIGA_AUTH) + try { + const result = await moderationText('', messageText, GIGA_AUTH); + [comment, isApproved, finalMessage] = result; + } catch (moderationError) { + console.error(`❌ [Moderation] Ошибка при вызове AI агента:`, moderationError); + console.error(`❌ [Moderation] Stack trace:`, moderationError.stack); + // В случае ошибки одобряем сообщение + comment = ''; + isApproved = true; + finalMessage = messageText; + console.log(`⚠️ [Moderation] Используем fallback значения из-за ошибки`); + } + + const moderationTime = Date.now() - moderationStartTime; + + if (isApproved) { + console.log(`📝 [Moderation] Действие: сообщение остается без изменений`); + } else { + console.log(`📝 [Moderation] Действие: сообщение будет заменено в базе данных`); + } + + // Если сообщение не прошло модерацию, обновляем его в базе данных + if (!isApproved) { + console.log(`💾 [Moderation] Начинаем обновление сообщения в базе данных...`); + + const supabase = getSupabaseClient(); + + // Сначала получаем информацию о сообщении для получения chat_id + console.log(`💾 [Moderation] Получаем данные сообщения из базы...`); + const { data: messageData, error: fetchError } = await supabase + .from('messages') + .select('chat_id, user_id') + .eq('id', messageId) + .single(); + + if (fetchError) { + console.error(`❌ [Moderation] Ошибка получения данных сообщения ${messageId}:`, fetchError); + return; + } + + console.log(`💾 [Moderation] Данные получены. Chat ID: ${messageData.chat_id}, User ID: ${messageData.user_id}`); + + // Обновляем текст сообщения + console.log(`💾 [Moderation] Обновляем текст сообщения на: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}"`); + const { data: updatedMessage, error } = await supabase + .from('messages') + .update({ text: MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT }) + .eq('id', messageId) + .select('*') + .single(); + + if (error) { + console.error(`❌ [Moderation] Ошибка обновления сообщения ${messageId}:`, error); + console.error(`❌ [Moderation] Детали ошибки:`, error); + } + } + + + } catch (error) { + const totalTime = Date.now() - moderationStartTime; + console.error(`❌ [Moderation] === ОШИБКА МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); + console.error(`❌ [Moderation] Время до ошибки: ${totalTime}мс`); + console.error(`❌ [Moderation] Тип ошибки: ${error.name || 'Unknown'}`); + console.error(`❌ [Moderation] Сообщение ошибки: ${error.message || 'Unknown error'}`); + console.error(`❌ [Moderation] Stack trace:`, error.stack); + } + } + // Получение статистики подключений getConnectionStats() { return { diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index 0568afa..2ddeb2f 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -6,10 +6,8 @@ let supabase = null; let initializationPromise = null; async function initSupabaseClient() { - console.log('🔄 [Supabase Client] Начинаем инициализацию...'); try { - console.log('🔄 [Supabase Client] Получаем конфигурацию...'); const supabaseUrl = await getSupabaseUrl(); const supabaseAnonKey = await getSupabaseKey(); const supabaseServiceRoleKey = await getSupabaseServiceKey(); @@ -49,7 +47,6 @@ router.post('/refresh-supabase-client', async (req, res) => { // GET /supabase-client-status router.get('/supabase-client-status', (req, res) => { - console.log('🔍 [Supabase Client] Проверяем статус клиента...'); const isInitialized = !!supabase;