feature/worker #111
@@ -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<ModerationResult> {
|
||||
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];
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -20,6 +26,7 @@ const moderateRouter = require('./moderate.js');
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
router.use('/auth', authRouter);
|
||||
router.use('/supabase', supabaseRouter);
|
||||
router.use('', profileRouter);
|
||||
@@ -29,7 +36,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('', avatarRouter);
|
||||
router.use('', supportRouter);
|
||||
router.use('', moderateRouter);
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
53
server/routers/kfu-m-24-1/sber_mobile/moderation.js
Normal file
53
server/routers/kfu-m-24-1/sber_mobile/moderation.js
Normal file
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user