From 09174abaa42bf581951305d2829c368dbfda9f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Thu, 12 Jun 2025 19:39:57 +0300 Subject: [PATCH 01/10] add ai_initiatives --- .../initiatives-ai-agents/moderation.ts | 57 +++++++++++++++ .../initiatives-ai-agents/picture.ts | 37 ++++++++++ .../kfu-m-24-1/sber_mobile/moderate.js | 73 +++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderate.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts new file mode 100644 index 0000000..00e5e0b --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -0,0 +1,57 @@ +import { Agent } from 'node:https'; +import { GigaChat } from "langchain-gigachat"; +import { z } from "zod"; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +const llm = new GigaChat({ + credentials: process.env.GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2', + httpsAgent, +}); + +// возвращаю комментарий + исправленное предложение + булево значение +const moderationLlm = llm.withStructuredOutput(z.object({ + comment: z.string(), + fixedText: z.string().optional(), + isApproved: z.boolean(), +}) as any) + +export const moderationText = async (title: string, body: string): Promise<[string, string | undefined, boolean]> => { + const prompt = ` + Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, + не имеющая отношения к Управляющей компании). + + Заголовок: ${title} + Основной текст: ${body} + + Твои задачи: + 1. Проверь предложение и заголовок на спам. + 2. Проверь, чтобы заголовок и текст были на одну тему. + 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. + 4. Проверь грамматику. + 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. + 6. Не должно быть рекламы, ссылок и т.д. + 7. Проверь предложение на информативность, оно не должно быть слишком коротким. + 8. Предложение должно быть в вежливой форме. + + - Если все правила соблюдены, то предложение принимается! + + Правила написания комментария: + - Если предложение отклоняется, верни комментарий со следующей формулировкой: + "Предложение отклонено. Причина: (укажи проблему)" + Правила написания fixedBody: + - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. + - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, + которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. + - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, + по которому оно не прошло. + -Если предложение принимается, то ничего не возвращай в поле fixedBody. + ` + const result = await moderationLlm.invoke(prompt); + + return [result.comment, result.fixedText, result.isApproved]; +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts new file mode 100644 index 0000000..febea0b --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -0,0 +1,37 @@ +import { GigaChat, detectImage } from 'gigachat'; +import { Agent } from 'node:https'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +export const llm = new GigaChat({ + credentials: process.env.GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); + +export const generatePicture = async (prompt: string) => { + const resp = await llm.chat({ + messages: [ + { + "role": "system", + "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" + }, + { + role: "user", + content: `Старайся передать атмосферу уюта и безопасности. + Нарисуй картинку подходящую для такого события: ${prompt} + В картинке не должно быть текста, только изображение.`, + }, + ], + function_call: 'auto', + }); + + // Получение изображения по идентификатору + const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); + const image = await llm.getImage(detectedImage?.uuid ?? ''); + + // Возвращаем содержимое изображения + return image.content; +} diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js new file mode 100644 index 0000000..76cccd3 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -0,0 +1,73 @@ +const router = require('express').Router(); +const { moderationText } = require('./initiatives-ai-agents/moderation'); +const { generatePicture } = require('./initiatives-ai-agents/picture'); +const { getSupabaseClient } = require('./supabaseClient'); + +// Обработчик для модерации текста +router.post('/moderate', async (req, res) => { + try { + const { title, body } = req.body; + if (!title || !body) { + res.status(400).json({ error: 'Заголовок и текст обязательны' }); + return; + } + + const [comment, fixedText, isApproved] = await moderationText(title, body); + res.json({ + comment, + fixedText, + isApproved + }); + } catch (error) { + res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } +}); + +// Обработчик для генерации изображений +router.post('/generate-image', async (req, res) => { + try { + const { prompt, userId } = req.body; + if (!prompt) { + res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); + return; + } + + // Получаем изображение + const imageBuffer = await generatePicture(prompt); + + // Получаем Supabase клиент + const supabase = getSupabaseClient(); + + // Генерируем уникальное имя файла + const timestamp = Date.now(); + const filename = `image_${userId || 'user'}_${timestamp}.jpg`; + + // Загружаем в Supabase + const { data, error } = await supabase.storage + .from('images') + .upload(filename, imageBuffer, { + contentType: 'image/jpeg', + upsert: true + }); + + if (error) { + res.status(500).json({ error: 'Ошибка при сохранении изображения' }); + return; + } + + // Получаем публичный URL изображения + const { data: urlData } = supabase.storage + .from('images') + .getPublicUrl(filename); + + res.json({ + success: true, + imageUrl: urlData.publicUrl, + imagePath: filename + }); + } catch (error) { + res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } +}); + +module.exports = router; \ No newline at end of file From 548dbfcc9dfcb49a3a40c5ecc469b66ad6787ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Thu, 12 Jun 2025 20:48:56 +0300 Subject: [PATCH 02/10] fix error --- server/routers/kfu-m-24-1/sber_mobile/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 e419be7..98ba2e2 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,6 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); +const moderateRouter = require('./moderate'); module.exports = router; @@ -33,4 +34,5 @@ router.use('', apartmentsRouter); router.use('', buildingsRouter); router.use('', userApartmentsRouter); router.use('', avatarRouter); -router.use('', supportRouter); \ No newline at end of file +router.use('', supportRouter); +router.use('', moderateRouter); \ No newline at end of file From 24ff71230641ee4167d4fa422b49529f23876b8b Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Thu, 12 Jun 2025 21:04:12 +0300 Subject: [PATCH 03/10] add sockets and change subscription --- server/index.ts | 13 +- server/io.ts | 9 +- .../routers/kfu-m-24-1/sber_mobile/chats.js | 22 +-- .../routers/kfu-m-24-1/sber_mobile/index.js | 1 + .../kfu-m-24-1/sber_mobile/messages.js | 90 ++++++----- .../kfu-m-24-1/sber_mobile/socket-chat.js | 147 ++++++++++++++++-- 6 files changed, 199 insertions(+), 83 deletions(-) diff --git a/server/index.ts b/server/index.ts index d39f65f..3d4f58e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,7 +20,9 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' -import { setIo } from './io' +import { setIo, getIo } from './io' +// Импорт обработчика чата +const { initializeChatSocket } = require('./routers/kfu-m-24-1/sber_mobile/socket-chat') export const app = express() @@ -65,6 +67,15 @@ const initServer = async () => { console.log('warming up 🔥') const server = setIo(app) + + // Инициализация Socket.IO для чата + const io = getIo() + if (io) { + const chatHandler = initializeChatSocket(io) + // Сохраняем ссылку на chat handler для доступа из эндпоинтов + io.chatHandler = chatHandler + console.log('✅ Socket.IO для чата инициализирован') + } const sess = { secret: "super-secret-key", diff --git a/server/io.ts b/server/io.ts index 71833d6..4121b06 100644 --- a/server/io.ts +++ b/server/io.ts @@ -5,7 +5,14 @@ let io = null export const setIo = (app) => { const server = createServer(app) - io = new Server(server, {}) + io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"], + credentials: false + }, + transports: ['websocket', 'polling'] + }) return server } diff --git a/server/routers/kfu-m-24-1/sber_mobile/chats.js b/server/routers/kfu-m-24-1/sber_mobile/chats.js index a20d9df..047ac37 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chats.js +++ b/server/routers/kfu-m-24-1/sber_mobile/chats.js @@ -3,31 +3,22 @@ const { getSupabaseClient } = require('./supabaseClient'); // Получить все чаты по дому router.get('/chats', async (req, res) => { - console.log('🏠 [Server] GET /chats запрос получен'); - console.log('🏠 [Server] Query параметры:', req.query); - const supabase = getSupabaseClient(); const { building_id } = req.query; if (!building_id) { - console.log('❌ [Server] Ошибка: building_id обязателен'); return res.status(400).json({ error: 'building_id required' }); } try { - console.log('🔍 [Server] Выполняем запрос к Supabase для здания:', building_id); - const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); if (error) { - console.log('❌ [Server] Ошибка Supabase:', error); return res.status(400).json({ error: error.message }); } - console.log('✅ [Server] Чаты получены:', data?.length || 0, 'шт.'); res.json(data || []); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка:', err); res.status(500).json({ error: 'Internal server error' }); } }); @@ -184,15 +175,10 @@ router.get('/chats/:chat_id/stats', async (req, res) => { // Получить последнее сообщение в чате router.get('/chats/:chat_id/last-message', async (req, res) => { - console.log('💬 [Server] GET /chats/:chat_id/last-message запрос получен'); - console.log('💬 [Server] Chat ID:', req.params.chat_id); - const supabase = getSupabaseClient(); const { chat_id } = req.params; try { - console.log('🔍 [Server] Выполняем запрос последнего сообщения для чата:', chat_id); - // Получаем последнее сообщение const { data: lastMessage, error } = await supabase .from('messages') @@ -205,10 +191,8 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { let data = null; if (error && error.code === 'PGRST116') { - console.log('ℹ️ [Server] Сообщений в чате нет (PGRST116)'); data = null; } else if (error) { - console.log('❌ [Server] Ошибка Supabase при получении последнего сообщения:', error); return res.status(400).json({ error: error.message }); } else if (lastMessage) { // Получаем профиль пользователя для сообщения @@ -223,12 +207,10 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { ...lastMessage, user_profiles: userProfile || null }; - console.log('✅ [Server] Последнее сообщение получено для чата:', chat_id); - } + } - res.json(data); + res.json(data); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка при получении последнего сообщения:', err); res.status(500).json({ error: 'Internal server error' }); } }); 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 e419be7..4dfad13 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,6 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); +const { getIo } = require('../../../io'); module.exports = router; 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 6c0bfa3..f729679 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,64 +1,59 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); +const { getIo } = require('../../../io'); // Импортируем Socket.IO // Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { - console.log('📬 [Server] GET /messages запрос получен'); - console.log('📬 [Server] Query параметры:', req.query); - - const supabase = getSupabaseClient(); - const { chat_id, limit = 50, offset = 0 } = req.query; - - if (!chat_id) { - console.log('❌ [Server] Ошибка: chat_id обязателен'); - return res.status(400).json({ error: 'chat_id required' }); - } - try { - console.log('🔍 [Server] Выполняем запрос к Supabase для чата:', chat_id); - - // Получаем сообщения - const { data: messages, error } = await supabase - .from('messages') - .select('*') - .eq('chat_id', chat_id) - .order('created_at', { ascending: false }) - .limit(limit) - .range(offset, offset + limit - 1); - - if (error) { - console.log('❌ [Server] Ошибка получения сообщений:', error); - return res.status(400).json({ error: error.message }); + const { chat_id, limit = 50, offset = 0 } = req.query; + + if (!chat_id) { + return res.status(400).json({ error: 'chat_id is required' }); } + + const supabase = getSupabaseClient(); - // Получаем профили пользователей для всех уникальных user_id - let data = messages || []; - if (data.length > 0) { - const userIds = [...new Set(data.map(msg => msg.user_id))]; - console.log('👥 [Server] Получаем профили для пользователей:', userIds); - + const { data, error } = await supabase + .from('messages') + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .eq('chat_id', chat_id) + .order('created_at', { ascending: true }) + .range(offset, offset + limit - 1); + + if (error) { + return res.status(500).json({ error: 'Failed to fetch messages' }); + } + + // Получаем уникальные ID пользователей из сообщений, у которых нет профиля + const messagesWithoutProfiles = data.filter(msg => !msg.user_profiles); + const userIds = [...new Set(messagesWithoutProfiles.map(msg => msg.user_id))]; + + if (userIds.length > 0) { const { data: profiles, error: profilesError } = await supabase .from('user_profiles') .select('id, full_name, avatar_url') .in('id', userIds); - + if (!profilesError && profiles) { - // Объединяем сообщения с профилями - data = data.map(msg => ({ - ...msg, - user_profiles: profiles.find(profile => profile.id === msg.user_id) || null - })); - console.log('✅ [Server] Профили пользователей добавлены к сообщениям'); - } else { - console.log('⚠️ [Server] Ошибка получения профилей пользователей:', profilesError); + // Добавляем профили к сообщениям + data.forEach(message => { + if (!message.user_profiles) { + message.user_profiles = profiles.find(profile => profile.id === message.user_id) || null; + } + }); } - } - - console.log('✅ [Server] Сообщения получены:', data?.length || 0, 'шт.'); - res.json(data?.reverse() || []); // Возвращаем в хронологическом порядке + } + + res.json(data); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка:', err); - res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({ error: 'Unexpected error occurred' }); } }); @@ -94,6 +89,9 @@ router.post('/messages', async (req, res) => { ...newMessage, user_profiles: userProfile || null }; + + // Отправка через Socket.IO теперь происходит автоматически через Supabase Real-time подписку + // Это предотвращает дублирование сообщений res.json(data); }); diff --git a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js index d35c2a2..62102c5 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js +++ b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js @@ -5,13 +5,29 @@ class ChatSocketHandler { this.io = io; this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) + this.realtimeSubscription = null; // Ссылка на подписку для управления + this.setupSocketHandlers(); + + try { + this.setupRealtimeSubscription(); // Добавляем Real-time подписки + } catch (error) { + // Ignore error + } + + // Запускаем тестирование через 2 секунды после инициализации + setTimeout(() => { + this.testRealtimeConnection(); + }, 2000); + + // Проверяем статус подписки через 5 секунд + setTimeout(() => { + this.checkSubscriptionStatus(); + }, 5000); } setupSocketHandlers() { this.io.on('connection', (socket) => { - console.log(`User connected: ${socket.id}`); - // Аутентификация пользователя socket.on('authenticate', async (data) => { await this.handleAuthentication(socket, data); @@ -84,10 +100,7 @@ class ChatSocketHandler { message: 'Successfully authenticated', user: userProfile }); - - console.log(`User ${user_id} authenticated with socket ${socket.id}`); } catch (error) { - console.error('Authentication error:', error); socket.emit('auth_error', { message: 'Authentication failed' }); } } @@ -105,7 +118,6 @@ class ChatSocketHandler { socket.emit('error', { message: 'chat_id is required' }); return; } - // Проверяем, что чат существует и пользователь имеет доступ к нему const supabase = getSupabaseClient(); const { data: chat, error } = await supabase @@ -140,7 +152,6 @@ class ChatSocketHandler { socket.emit('error', { message: 'Access denied to this chat' }); return; } - // Добавляем сокет в комнату socket.join(chat_id); @@ -148,8 +159,11 @@ class ChatSocketHandler { if (!this.chatRooms.has(chat_id)) { this.chatRooms.set(chat_id, new Set()); } + + const participantsBefore = this.chatRooms.get(chat_id).size; this.chatRooms.get(chat_id).add(socket.id); - + const participantsAfter = this.chatRooms.get(chat_id).size; + socket.emit('joined_chat', { chat_id, chat: chat, @@ -158,15 +172,13 @@ class ChatSocketHandler { // Уведомляем других участников о подключении const userInfo = this.onlineUsers.get(socket.id); + socket.to(chat_id).emit('user_joined', { chat_id, user: userInfo?.profile, timestamp: new Date() }); - - console.log(`User ${socket.user_id} joined chat ${chat_id}`); } catch (error) { - console.error('Join chat error:', error); socket.emit('error', { message: 'Failed to join chat' }); } } @@ -196,7 +208,7 @@ class ChatSocketHandler { timestamp: new Date() }); - console.log(`User ${socket.user_id} left chat ${chat_id}`); + } async handleSendMessage(socket, data) { @@ -243,9 +255,7 @@ class ChatSocketHandler { timestamp: new Date() }); - console.log(`Message sent to chat ${chat_id} by user ${socket.user_id}`); } catch (error) { - console.error('Send message error:', error); socket.emit('error', { message: 'Failed to send message' }); } } @@ -277,7 +287,6 @@ class ChatSocketHandler { } handleDisconnect(socket) { - console.log(`User disconnected: ${socket.id}`); // Удаляем пользователя из всех комнат this.chatRooms.forEach((participants, chat_id) => { @@ -326,11 +335,119 @@ class ChatSocketHandler { timestamp: new Date() }); } + + // Тестирование Real-time подписки + async testRealtimeConnection() { + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return false; + } + + // Создаем тестовый канал для проверки подключения + const testChannel = supabase + .channel('test_connection') + .subscribe((status, error) => { + if (status === 'SUBSCRIBED') { + // Отписываемся от тестового канала + setTimeout(() => { + testChannel.unsubscribe(); + }, 2000); + } + }); + + return true; + } catch (error) { + return false; + } + } + + // Проверка статуса подписки + checkSubscriptionStatus() { + if (this.realtimeSubscription) { + return true; + } else { + return false; + } + } + + setupRealtimeSubscription() { + // Добавляем небольшую задержку, чтобы убедиться, что Supabase клиент инициализирован + setTimeout(() => { + this._doSetupRealtimeSubscription(); + }, 1000); + } + + _doSetupRealtimeSubscription() { + try { + const supabase = getSupabaseClient(); + + if (!supabase) { + return; + } + + // Подписываемся на изменения в таблице messages + const subscription = supabase + .channel('messages_changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const newMessage = payload.new; + if (!newMessage) { + return; + } + + if (!newMessage.chat_id) { + return; + } + + // Получаем профиль пользователя + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', newMessage.user_id) + .single(); + + // Объединяем сообщение с профилем + const messageWithProfile = { + ...newMessage, + user_profiles: userProfile || null + }; + + // Проверяем, есть ли участники в чате + const chatRoomParticipants = this.chatRooms.get(newMessage.chat_id); + + // Отправляем сообщение через Socket.IO всем участникам чата + this.io.to(newMessage.chat_id).emit('new_message', { + message: messageWithProfile, + timestamp: new Date() + }); + } catch (callbackError) { + // Ignore error + } + } + ) + .subscribe(); + + // Сохраняем ссылку на подписку для возможности отписки + this.realtimeSubscription = subscription; + + } catch (error) { + // Ignore error + } + } } // Функция инициализации Socket.IO для чатов function initializeChatSocket(io) { const chatHandler = new ChatSocketHandler(io); + return chatHandler; } From 39a62818e923e300c5182238d173b561cdfa3ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Thu, 12 Jun 2025 21:07:06 +0300 Subject: [PATCH 04/10] fix error --- .../routers/kfu-m-24-1/sber_mobile/index.js | 2 +- .../initiatives-ai-agents/moderation.js | 56 +++++++++++++++++++ .../initiatives-ai-agents/picture.js | 36 ++++++++++++ .../sber_mobile/{moderate.js => moderate.ts} | 25 +++++---- 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js rename server/routers/kfu-m-24-1/sber_mobile/{moderate.js => moderate.ts} (76%) 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 98ba2e2..a13e53e 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,7 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const moderateRouter = require('./moderate'); +const moderateRouter = require('./moderate.ts').default; module.exports = router; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js new file mode 100644 index 0000000..88896b8 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js @@ -0,0 +1,56 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.moderationText = void 0; +const node_https_1 = require("node:https"); +const langchain_gigachat_1 = require("langchain-gigachat"); +const zod_1 = require("zod"); +const httpsAgent = new node_https_1.Agent({ + rejectUnauthorized: false, +}); +const llm = new langchain_gigachat_1.GigaChat({ + credentials: process.env.GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2', + httpsAgent, +}); +// возвращаю комментарий + исправленное предложение + булево значение +const moderationLlm = llm.withStructuredOutput(zod_1.z.object({ + comment: zod_1.z.string(), + fixedText: zod_1.z.string().optional(), + isApproved: zod_1.z.boolean(), +})); +const moderationText = async (title, body) => { + const prompt = ` + Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, + не имеющая отношения к Управляющей компании). + + Заголовок: ${title} + Основной текст: ${body} + + Твои задачи: + 1. Проверь предложение и заголовок на спам. + 2. Проверь, чтобы заголовок и текст были на одну тему. + 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. + 4. Проверь грамматику. + 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. + 6. Не должно быть рекламы, ссылок и т.д. + 7. Проверь предложение на информативность, оно не должно быть слишком коротким. + 8. Предложение должно быть в вежливой форме. + + - Если все правила соблюдены, то предложение принимается! + + Правила написания комментария: + - Если предложение отклоняется, верни комментарий со следующей формулировкой: + "Предложение отклонено. Причина: (укажи проблему)" + Правила написания fixedBody: + - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. + - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, + которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. + - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, + по которому оно не прошло. + -Если предложение принимается, то ничего не возвращай в поле fixedBody. + `; + const result = await moderationLlm.invoke(prompt); + return [result.comment, result.fixedText, result.isApproved]; +}; +exports.moderationText = moderationText; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js new file mode 100644 index 0000000..0b54adc --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js @@ -0,0 +1,36 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generatePicture = exports.llm = void 0; +const gigachat_1 = require("gigachat"); +const node_https_1 = require("node:https"); +const httpsAgent = new node_https_1.Agent({ + rejectUnauthorized: false, +}); +exports.llm = new gigachat_1.GigaChat({ + credentials: process.env.GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); +const generatePicture = async (prompt) => { + const resp = await exports.llm.chat({ + messages: [ + { + "role": "system", + "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" + }, + { + role: "user", + content: `Старайся передать атмосферу уюта и безопасности. + Нарисуй картинку подходящую для такого события: ${prompt} + В картинке не должно быть текста, только изображение.`, + }, + ], + function_call: 'auto', + }); + // Получение изображения по идентификатору + const detectedImage = (0, gigachat_1.detectImage)(resp.choices[0]?.message.content ?? ''); + const image = await exports.llm.getImage(detectedImage?.uuid ?? ''); + // Возвращаем содержимое изображения + return image.content; +}; +exports.generatePicture = generatePicture; diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.ts similarity index 76% rename from server/routers/kfu-m-24-1/sber_mobile/moderate.js rename to server/routers/kfu-m-24-1/sber_mobile/moderate.ts index 76cccd3..332554a 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.js +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.ts @@ -1,10 +1,13 @@ -const router = require('express').Router(); -const { moderationText } = require('./initiatives-ai-agents/moderation'); -const { generatePicture } = require('./initiatives-ai-agents/picture'); +import { Router, Request, Response } from 'express'; +import { moderationText } from './initiatives-ai-agents/moderation'; +import { generatePicture } from './initiatives-ai-agents/picture'; + const { getSupabaseClient } = require('./supabaseClient'); +const router = Router(); + // Обработчик для модерации текста -router.post('/moderate', async (req, res) => { +router.post('/moderate', async (req: Request, res: Response) => { try { const { title, body } = req.body; if (!title || !body) { @@ -18,13 +21,14 @@ router.post('/moderate', async (req, res) => { fixedText, isApproved }); - } catch (error) { - res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } catch (error: any) { + console.error('Error in moderation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); } }); // Обработчик для генерации изображений -router.post('/generate-image', async (req, res) => { +router.post('/generate-image', async (req: Request, res: Response) => { try { const { prompt, userId } = req.body; if (!prompt) { @@ -65,9 +69,10 @@ router.post('/generate-image', async (req, res) => { imageUrl: urlData.publicUrl, imagePath: filename }); - } catch (error) { - res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } catch (error: any) { + console.error('Error in image generation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); } }); -module.exports = router; \ No newline at end of file +export default router; \ No newline at end of file From 7bd82fedced6f62bd7bd555de797dd90659b9f68 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Fri, 13 Jun 2025 23:52:02 +0300 Subject: [PATCH 05/10] change socket settings --- server/index.ts | 13 +------------ server/routers/kfu-m-24-1/sber_mobile/index.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/server/index.ts b/server/index.ts index 3d4f58e..d39f65f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,9 +20,7 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' -import { setIo, getIo } from './io' -// Импорт обработчика чата -const { initializeChatSocket } = require('./routers/kfu-m-24-1/sber_mobile/socket-chat') +import { setIo } from './io' export const app = express() @@ -67,15 +65,6 @@ const initServer = async () => { console.log('warming up 🔥') const server = setIo(app) - - // Инициализация Socket.IO для чата - const io = getIo() - if (io) { - const chatHandler = initializeChatSocket(io) - // Сохраняем ссылку на chat handler для доступа из эндпоинтов - io.chatHandler = chatHandler - console.log('✅ Socket.IO для чата инициализирован') - } const sess = { secret: "super-secret-key", 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 4dfad13..854576e 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,7 +15,10 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const { getIo } = require('../../../io'); +const {setIo, getIo } = require('../../../io'); +// Импорт обработчика чата +const { initializeChatSocket } = require('./socket-chat') + module.exports = router; @@ -34,4 +37,14 @@ router.use('', apartmentsRouter); router.use('', buildingsRouter); router.use('', userApartmentsRouter); router.use('', avatarRouter); -router.use('', supportRouter); \ No newline at end of file +router.use('', supportRouter); + + + // Инициализация Socket.IO для чата + const io = getIo() + if (io) { + const chatHandler = initializeChatSocket(io) + // Сохраняем ссылку на chat handler для доступа из эндпоинтов + io.chatHandler = chatHandler + console.log('✅ Socket.IO для чата инициализирован') + } \ No newline at end of file From a7be7936085d1af15b4ddc46ba485c3c53fa1200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Sat, 14 Jun 2025 02:01:19 +0300 Subject: [PATCH 06/10] change file type and fix agents --- .../routers/kfu-m-24-1/sber_mobile/index.js | 2 +- .../initiatives-ai-agents/moderation.js | 56 -------- .../initiatives-ai-agents/moderation.ts | 24 +++- .../initiatives-ai-agents/picture.js | 36 ----- .../initiatives-ai-agents/picture.ts | 18 ++- .../kfu-m-24-1/sber_mobile/moderate.js | 124 ++++++++++++++++++ .../kfu-m-24-1/sber_mobile/moderate.ts | 78 ----------- 7 files changed, 157 insertions(+), 181 deletions(-) delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderate.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderate.ts 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 a13e53e..da592c7 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,7 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const moderateRouter = require('./moderate.ts').default; +const moderateRouter = require('./moderate.js'); module.exports = router; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js deleted file mode 100644 index 88896b8..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.moderationText = void 0; -const node_https_1 = require("node:https"); -const langchain_gigachat_1 = require("langchain-gigachat"); -const zod_1 = require("zod"); -const httpsAgent = new node_https_1.Agent({ - rejectUnauthorized: false, -}); -const llm = new langchain_gigachat_1.GigaChat({ - credentials: process.env.GIGA_AUTH, - temperature: 0.2, - model: 'GigaChat-2', - httpsAgent, -}); -// возвращаю комментарий + исправленное предложение + булево значение -const moderationLlm = llm.withStructuredOutput(zod_1.z.object({ - comment: zod_1.z.string(), - fixedText: zod_1.z.string().optional(), - isApproved: zod_1.z.boolean(), -})); -const moderationText = async (title, body) => { - const prompt = ` - Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, - не имеющая отношения к Управляющей компании). - - Заголовок: ${title} - Основной текст: ${body} - - Твои задачи: - 1. Проверь предложение и заголовок на спам. - 2. Проверь, чтобы заголовок и текст были на одну тему. - 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. - 4. Проверь грамматику. - 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. - 6. Не должно быть рекламы, ссылок и т.д. - 7. Проверь предложение на информативность, оно не должно быть слишком коротким. - 8. Предложение должно быть в вежливой форме. - - - Если все правила соблюдены, то предложение принимается! - - Правила написания комментария: - - Если предложение отклоняется, верни комментарий со следующей формулировкой: - "Предложение отклонено. Причина: (укажи проблему)" - Правила написания fixedBody: - - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. - - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, - которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. - - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, - по которому оно не прошло. - -Если предложение принимается, то ничего не возвращай в поле fixedBody. - `; - const result = await moderationLlm.invoke(prompt); - return [result.comment, result.fixedText, result.isApproved]; -}; -exports.moderationText = moderationText; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts index 00e5e0b..7a34fdb 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -35,23 +35,33 @@ export const moderationText = async (title: string, body: string): Promise<[stri 4. Проверь грамматику. 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. 6. Не должно быть рекламы, ссылок и т.д. - 7. Проверь предложение на информативность, оно не должно быть слишком коротким. + 7. Проверь предложение на информативность, предложение не может быть коротким, оно должно ясно отражжать суть инициативы. 8. Предложение должно быть в вежливой форме. - Если все правила соблюдены, то предложение принимается! + - Если предложение отклонено, всегда пиши комментарий и fixedText! + Правила написания комментария: - - Если предложение отклоняется, верни комментарий со следующей формулировкой: + - Если предложение отклоняется, пиши комментарий со следующей формулировкой: "Предложение отклонено. Причина: (укажи проблему)" - Правила написания fixedBody: - - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. + + Правила написания fixedText: + - Если предложение отклонено, то верни в поле "fixedText" измененный текст, который будет соответствовать правилам. - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. - - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, + - Если текст не представляет никакой ценности, возврати в поле "fixedText" правило, по которому оно не прошло. - -Если предложение принимается, то ничего не возвращай в поле fixedBody. + -Если предложение принимается, то ничего не возвращай в поле fixedText. ` + const result = await moderationLlm.invoke(prompt); - + console.log(result) + // Дополнительная проверка + if(!result.isApproved && result.comment.trim() === '' && result.fixedText.trim() === '') { + result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', + result.fixedText = body + } + return [result.comment, result.fixedText, result.isApproved]; }; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js deleted file mode 100644 index 0b54adc..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePicture = exports.llm = void 0; -const gigachat_1 = require("gigachat"); -const node_https_1 = require("node:https"); -const httpsAgent = new node_https_1.Agent({ - rejectUnauthorized: false, -}); -exports.llm = new gigachat_1.GigaChat({ - credentials: process.env.GIGA_AUTH, - model: 'GigaChat-2', - httpsAgent, -}); -const generatePicture = async (prompt) => { - const resp = await exports.llm.chat({ - messages: [ - { - "role": "system", - "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" - }, - { - role: "user", - content: `Старайся передать атмосферу уюта и безопасности. - Нарисуй картинку подходящую для такого события: ${prompt} - В картинке не должно быть текста, только изображение.`, - }, - ], - function_call: 'auto', - }); - // Получение изображения по идентификатору - const detectedImage = (0, gigachat_1.detectImage)(resp.choices[0]?.message.content ?? ''); - const image = await exports.llm.getImage(detectedImage?.uuid ?? ''); - // Возвращаем содержимое изображения - return image.content; -}; -exports.generatePicture = generatePicture; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts index febea0b..2544dd3 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -3,6 +3,7 @@ import { Agent } from 'node:https'; const httpsAgent = new Agent({ rejectUnauthorized: false, + timeout: 60000 }); export const llm = new GigaChat({ @@ -30,8 +31,19 @@ export const generatePicture = async (prompt: string) => { // Получение изображения по идентификатору const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); - const image = await llm.getImage(detectedImage?.uuid ?? ''); + + if (!detectedImage?.uuid) { + throw new Error('Не удалось получить UUID изображения из ответа GigaChat'); + } + + const image = await llm.getImage(detectedImage.uuid); - // Возвращаем содержимое изображения - return image.content; + // Возвращаем содержимое изображения, убеждаясь что это Buffer + if (Buffer.isBuffer(image.content)) { + return image.content; + } else if (typeof image.content === 'string') { + return Buffer.from(image.content, 'binary'); + } else { + throw new Error('Unexpected image content type: ' + typeof image.content); + } } diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js new file mode 100644 index 0000000..25b5e3a --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -0,0 +1,124 @@ +const router = require('express').Router(); +const { moderationText } = require('./initiatives-ai-agents/moderation.ts'); +const { generatePicture } = require('./initiatives-ai-agents/picture.ts'); +const { getSupabaseClient } = require('./supabaseClient'); + +// Обработчик для модерации текста +router.post('/moderate', async (req, res) => { + try { + const { title, body } = req.body; + if (!title || !body) { + res.status(400).json({ error: 'Заголовок и текст обязательны' }); + return; + } + + console.log('Запрос на модерацию:', { title: title.substring(0, 50), body: body.substring(0, 100) }); + + const [comment, fixedText, isApproved] = await moderationText(title, body); + + console.log('Результат модерации получен:', { comment, fixedText: fixedText?.substring(0, 100), isApproved }); + + // Дополнительная проверка на стороне сервера + if (!isApproved && (!comment || comment.trim() === '')) { + console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении'); + } + + res.json({ + comment, + fixedText, + isApproved + }); + } catch (error) { + console.error('Error in moderation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); + } +}); + +// Обработчик для генерации изображений +router.post('/generate-image', async (req, res) => { + try { + const { prompt, userId } = req.body; + if (!prompt) { + res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); + return; + } + + // Генерируем изображение + const imageBuffer = await generatePicture(prompt); + + //console.log('Изображение получено, размер буфера:', imageBuffer?.length || 0, 'байт'); + if (!imageBuffer || imageBuffer.length === 0) { + res.status(500).json({ error: 'Получен пустой буфер изображения' }); + return; + } + + //console.log('Начинаем загрузку в Supabase Storage...'); + + // Получаем Supabase клиент и создаем имя файла + const supabase = getSupabaseClient(); + const timestamp = Date.now(); + const filename = `image_${userId || 'user'}_${timestamp}.jpg`; + + let uploadResult; + let retries = 0; + const maxRetries = 5; + + while (retries < maxRetries) { + try { + uploadResult = await supabase.storage + .from('images') + .upload(filename, imageBuffer, { + contentType: 'image/jpeg', + upsert: true + }); + + if (!uploadResult.error) { + break; // Успешная загрузка + } + + //console.warn(`Попытка загрузки ${retries + 1} неудачна:`, uploadResult.error); + retries++; + + if (retries < maxRetries) { + // Ждем перед повторной попыткой + await new Promise(resolve => setTimeout(resolve, 1000 * retries)); + } + } catch (error) { + //console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message); + retries++; + + if (retries < maxRetries) { + // Ждем перед повторной попыткой + await new Promise(resolve => setTimeout(resolve, 1000 * retries)); + } else { + throw error; // Перебрасываем ошибку после всех попыток + } + } + } + + if (uploadResult?.error) { + //console.error('Supabase storage error after all retries:', uploadResult.error); + res.status(500).json({ error: 'Ошибка при сохранении изображения после нескольких попыток' }); + return; + } + + //console.log('Изображение успешно загружено в Supabase Storage:', filename); + + // Получаем публичный URL + const { data: urlData } = supabase.storage + .from('images') + .getPublicUrl(filename); + + res.json({ + success: true, + imageUrl: urlData.publicUrl, + imagePath: filename + }); + + } catch (error) { + //console.error('Error in image generation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.ts b/server/routers/kfu-m-24-1/sber_mobile/moderate.ts deleted file mode 100644 index 332554a..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Router, Request, Response } from 'express'; -import { moderationText } from './initiatives-ai-agents/moderation'; -import { generatePicture } from './initiatives-ai-agents/picture'; - -const { getSupabaseClient } = require('./supabaseClient'); - -const router = Router(); - -// Обработчик для модерации текста -router.post('/moderate', async (req: Request, res: Response) => { - try { - const { title, body } = req.body; - if (!title || !body) { - res.status(400).json({ error: 'Заголовок и текст обязательны' }); - return; - } - - const [comment, fixedText, isApproved] = await moderationText(title, body); - res.json({ - comment, - fixedText, - isApproved - }); - } catch (error: any) { - console.error('Error in moderation:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); - } -}); - -// Обработчик для генерации изображений -router.post('/generate-image', async (req: Request, res: Response) => { - try { - const { prompt, userId } = req.body; - if (!prompt) { - res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); - return; - } - - // Получаем изображение - const imageBuffer = await generatePicture(prompt); - - // Получаем Supabase клиент - const supabase = getSupabaseClient(); - - // Генерируем уникальное имя файла - const timestamp = Date.now(); - const filename = `image_${userId || 'user'}_${timestamp}.jpg`; - - // Загружаем в Supabase - const { data, error } = await supabase.storage - .from('images') - .upload(filename, imageBuffer, { - contentType: 'image/jpeg', - upsert: true - }); - - if (error) { - res.status(500).json({ error: 'Ошибка при сохранении изображения' }); - return; - } - - // Получаем публичный URL изображения - const { data: urlData } = supabase.storage - .from('images') - .getPublicUrl(filename); - - res.json({ - success: true, - imageUrl: urlData.publicUrl, - imagePath: filename - }); - } catch (error: any) { - console.error('Error in image generation:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); - } -}); - -export default router; \ No newline at end of file From bde67dc7c310a1e542d2d378267ae9ef78b82a72 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 14 Jun 2025 10:30:12 +0300 Subject: [PATCH 07/10] fix socket server --- server/io.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/server/io.ts b/server/io.ts index 4121b06..71833d6 100644 --- a/server/io.ts +++ b/server/io.ts @@ -5,14 +5,7 @@ let io = null export const setIo = (app) => { const server = createServer(app) - io = new Server(server, { - cors: { - origin: "*", - methods: ["GET", "POST"], - credentials: false - }, - transports: ['websocket', 'polling'] - }) + io = new Server(server, {}) return server } From 580651094f42183c44d45b919cd5260c5c1d591f Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 14 Jun 2025 13:36:06 +0300 Subject: [PATCH 08/10] remove websocket add polling --- server/index.ts | 18 +- .../routers/kfu-m-24-1/sber_mobile/index.js | 14 +- .../kfu-m-24-1/sber_mobile/polling-chat.js | 822 ++++++++++++++++++ .../kfu-m-24-1/sber_mobile/socket-chat.js | 457 ---------- .../kfu-m-24-1/sber_mobile/supabaseClient.js | 65 +- 5 files changed, 894 insertions(+), 482 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/polling-chat.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/socket-chat.js diff --git a/server/index.ts b/server/index.ts index d39f65f..7f1d913 100644 --- a/server/index.ts +++ b/server/index.ts @@ -21,6 +21,7 @@ import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' import { setIo } from './io' +const { createChatPollingRouter } = require('./routers/kfu-m-24-1/sber_mobile/polling-chat') export const app = express() @@ -64,8 +65,6 @@ const initServer = async () => { console.log('warming up 🔥') - const server = setIo(app) - const sess = { secret: "super-secret-key", resave: true, @@ -90,10 +89,18 @@ const initServer = async () => { ) app.use(root) + // Инициализация Polling для чата (после настройки middleware) + const { router: chatPollingRouter, chatHandler } = createChatPollingRouter(express) + + /** * Добавляйте сюда свои routers. */ app.use("/kfu-m-24-1", kfuM241Router) + + // Добавляем Polling роутер для чата + app.use("/kfu-m-24-1/sber_mobile", chatPollingRouter) + app.use("/epja-2024-1", epja20241Router) app.use("/v1/todo", todoRouter) app.use("/dogsitters-finder", dogsittersFinderRouter) @@ -109,9 +116,10 @@ const initServer = async () => { app.use(errorHandler) - server.listen(process.env.PORT ?? 8044, () => + // Создаем обычный HTTP сервер + const server = app.listen(process.env.PORT ?? 8044, () => { console.log(`🚀 Сервер запущен на http://localhost:${process.env.PORT ?? 8044}`) - ) + }) // Обработка сигналов завершения процесса process.on('SIGTERM', () => { @@ -145,6 +153,8 @@ const initServer = async () => { process.exit(1) }) }) + + return server } initServer().catch(console.error) 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 854576e..2fdc6bd 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,9 +15,6 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const {setIo, getIo } = require('../../../io'); -// Импорт обработчика чата -const { initializeChatSocket } = require('./socket-chat') module.exports = router; @@ -39,12 +36,5 @@ router.use('', userApartmentsRouter); router.use('', avatarRouter); router.use('', supportRouter); - - // Инициализация Socket.IO для чата - const io = getIo() - if (io) { - const chatHandler = initializeChatSocket(io) - // Сохраняем ссылку на chat handler для доступа из эндпоинтов - io.chatHandler = chatHandler - console.log('✅ Socket.IO для чата инициализирован') - } \ 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 new file mode 100644 index 0000000..db528ca --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js @@ -0,0 +1,822 @@ +const { getSupabaseClient, initializationPromise } = require('./supabaseClient'); + +class ChatPollingHandler { + constructor() { + this.connectedClients = new Map(); // user_id -> { user_info, chats: Set(), lastActivity: Date } + this.chatParticipants = new Map(); // chat_id -> Set(user_id) + this.userEventQueues = new Map(); // user_id -> [{id, event, data, timestamp}] + this.eventIdCounter = 0; + this.realtimeSubscription = null; + + // Инициализируем Supabase подписку с задержкой и проверками + this.initializeWithRetry(); + + // Очистка старых событий каждые 5 минут + setInterval(() => { + this.cleanupOldEvents(); + }, 5 * 60 * 1000); + } + + // Инициализация с повторными попытками + async initializeWithRetry() { + try { + // Сначала ждем завершения основной инициализации + await initializationPromise; + + this.setupRealtimeSubscription(); + this.testRealtimeConnection(); + return; + + } catch (error) { + console.log('❌ [Supabase] Основная инициализация неудачна, пробуем альтернативный подход'); + } + + // Если основная инициализация не удалась, используем повторные попытки + let attempts = 0; + const maxAttempts = 10; + const baseDelay = 2000; // 2 секунды + + while (attempts < maxAttempts) { + try { + attempts++; + + // Ждем перед попыткой + await new Promise(resolve => setTimeout(resolve, baseDelay * attempts)); + + // Проверяем готовность Supabase клиента + const supabase = getSupabaseClient(); + if (supabase) { + this.setupRealtimeSubscription(); + this.testRealtimeConnection(); + return; // Успех, выходим + } + } catch (error) { + console.log(`❌ [Supabase] Попытка #${attempts} неудачна:`, error.message); + + if (attempts === maxAttempts) { + console.error('❌ [Supabase] Все попытки инициализации исчерпаны'); + console.error('❌ [Supabase] Realtime подписка будет недоступна'); + return; + } + } + } + } + + // Аутентификация пользователя + async handleAuthentication(req, res) { + const { user_id, token } = req.body; + + if (!user_id) { + res.status(400).json({ error: 'user_id is required' }); + return; + } + + try { + // Проверяем пользователя в базе данных + const supabase = getSupabaseClient(); + const { data: userProfile, error } = await supabase + .from('user_profiles') + .select('*') + .eq('id', user_id) + .single(); + + if (error) { + console.log('❌ [Polling Server] Пользователь не найден:', error); + res.status(401).json({ error: 'User not found' }); + return; + } + + // Регистрируем пользователя + this.connectedClients.set(user_id, { + user_info: { + user_id, + profile: userProfile, + last_seen: new Date() + }, + chats: new Set(), + lastActivity: new Date() + }); + + // Создаем очередь событий для пользователя + if (!this.userEventQueues.has(user_id)) { + this.userEventQueues.set(user_id, []); + } + + // Добавляем событие аутентификации в очередь + this.addEventToQueue(user_id, 'authenticated', { + message: 'Successfully authenticated', + user: userProfile + }); + + res.json({ + success: true, + message: 'Successfully authenticated', + user: userProfile + }); + + } catch (error) { + console.error('❌ [Polling Server] Ошибка аутентификации:', error); + res.status(500).json({ error: 'Authentication failed' }); + } + } + + // Эндпоинт для получения событий (polling) + async handleGetEvents(req, res) { + try { + const { user_id, last_event_id } = req.query; + + if (!user_id) { + res.status(400).json({ error: 'user_id is required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // Обновляем время последней активности + client.lastActivity = new Date(); + + // Получаем очередь событий пользователя + const eventQueue = this.userEventQueues.get(user_id) || []; + + // Фильтруем события после last_event_id + const lastEventId = parseInt(last_event_id) || 0; + const newEvents = eventQueue.filter(event => event.id > lastEventId); + + res.json({ + success: true, + events: newEvents, + last_event_id: eventQueue.length > 0 ? Math.max(...eventQueue.map(e => e.id)) : lastEventId + }); + + } catch (error) { + console.error('❌ [Polling Server] Ошибка получения событий:', error); + res.status(500).json({ error: 'Failed to get events' }); + } + } + + // HTTP эндпоинт для присоединения к чату + async handleJoinChat(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // Проверяем, что чат существует и пользователь имеет доступ к нему + const supabase = getSupabaseClient(); + const { data: chat, error } = await supabase + .from('chats') + .select(` + *, + buildings ( + management_company_id, + apartments ( + apartment_residents ( + user_id + ) + ) + ) + `) + .eq('id', chat_id) + .single(); + + if (error || !chat) { + res.status(404).json({ error: 'Chat not found' }); + return; + } + + // Проверяем доступ пользователя к чату через квартиры в доме + const hasAccess = chat.buildings.apartments.some(apartment => + apartment.apartment_residents.some(resident => + resident.user_id === user_id + ) + ); + + if (!hasAccess) { + res.status(403).json({ error: 'Access denied to this chat' }); + return; + } + + // Добавляем пользователя в чат + client.chats.add(chat_id); + + if (!this.chatParticipants.has(chat_id)) { + this.chatParticipants.set(chat_id, new Set()); + } + this.chatParticipants.get(chat_id).add(user_id); + + // Добавляем событие присоединения в очередь пользователя + this.addEventToQueue(user_id, 'joined_chat', { + chat_id, + chat: chat, + message: 'Successfully joined chat' + }); + + // Уведомляем других участников о подключении + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_joined', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Joined chat successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to join chat' }); + } + } + + // HTTP эндпоинт для покидания чата + async handleLeaveChat(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // Удаляем пользователя из чата + client.chats.delete(chat_id); + + if (this.chatParticipants.has(chat_id)) { + this.chatParticipants.get(chat_id).delete(user_id); + + // Если чат пуст, удаляем его + if (this.chatParticipants.get(chat_id).size === 0) { + this.chatParticipants.delete(chat_id); + } + } + + // Уведомляем других участников об отключении + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Left chat successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to leave chat' }); + } + } + + // HTTP эндпоинт для отправки сообщения + async handleSendMessage(req, res) { + try { + const { user_id, chat_id, text } = req.body; + + if (!user_id || !chat_id || !text) { + res.status(400).json({ error: 'user_id, chat_id and text are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + // Сохраняем сообщение в базу данных + const supabase = getSupabaseClient(); + const { data: message, error } = await supabase + .from('messages') + .insert({ + chat_id, + user_id, + text + }) + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .single(); + + if (error) { + res.status(500).json({ error: 'Failed to save message' }); + return; + } + + // Отправляем сообщение всем участникам чата + this.broadcastToChat(chat_id, 'new_message', { + message, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Message sent successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send message' }); + } + } + + // HTTP эндпоинт для индикации печатания + async handleTypingStart(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_start', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send typing indicator' }); + } + } + + // HTTP эндпоинт для остановки индикации печатания + async handleTypingStop(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_stop', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send typing indicator' }); + } + } + + // Обработка отключения клиента + handleClientDisconnect(user_id) { + const client = this.connectedClients.get(user_id); + if (!client) return; + + // Удаляем пользователя из всех чатов + client.chats.forEach(chat_id => { + if (this.chatParticipants.has(chat_id)) { + this.chatParticipants.get(chat_id).delete(user_id); + + // Уведомляем других участников об отключении + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + // Если чат пуст, удаляем его + if (this.chatParticipants.get(chat_id).size === 0) { + this.chatParticipants.delete(chat_id); + } + } + }); + + // Удаляем клиента + this.connectedClients.delete(user_id); + } + + // Добавление события в очередь пользователя + addEventToQueue(user_id, event, data) { + if (!this.userEventQueues.has(user_id)) { + this.userEventQueues.set(user_id, []); + } + + const eventQueue = this.userEventQueues.get(user_id); + const eventId = ++this.eventIdCounter; + + eventQueue.push({ + id: eventId, + event, + data, + timestamp: new Date() + }); + + // Ограничиваем размер очереди (последние 100 событий) + if (eventQueue.length > 100) { + eventQueue.splice(0, eventQueue.length - 100); + } + } + + // Рассылка события всем участникам чата + broadcastToChat(chat_id, event, data) { + const participants = this.chatParticipants.get(chat_id); + if (!participants) return; + + participants.forEach(user_id => { + this.addEventToQueue(user_id, event, data); + }); + } + + // Рассылка события всем участникам чата кроме отправителя + broadcastToChatExcludeUser(chat_id, exclude_user_id, event, data) { + const participants = this.chatParticipants.get(chat_id); + if (!participants) return; + + participants.forEach(user_id => { + if (user_id !== exclude_user_id) { + this.addEventToQueue(user_id, event, data); + } + }); + } + + // Получение списка онлайн пользователей в чате + getOnlineUsersInChat(chat_id) { + const participants = this.chatParticipants.get(chat_id) || new Set(); + const onlineUsers = []; + const now = new Date(); + const ONLINE_THRESHOLD = 2 * 60 * 1000; // 2 минуты + + participants.forEach(user_id => { + const client = this.connectedClients.get(user_id); + if (client && (now - client.lastActivity) < ONLINE_THRESHOLD) { + onlineUsers.push(client.user_info.profile); + } + }); + + return onlineUsers; + } + + // Отправка системного сообщения в чат + async sendSystemMessage(chat_id, text) { + this.broadcastToChat(chat_id, 'system_message', { + chat_id, + text, + timestamp: new Date() + }); + } + + // Очистка старых событий + cleanupOldEvents() { + const now = new Date(); + const MAX_EVENT_AGE = 24 * 60 * 60 * 1000; // 24 часа + const INACTIVE_USER_THRESHOLD = 60 * 60 * 1000; // 1 час + + // Очищаем старые события + this.userEventQueues.forEach((eventQueue, user_id) => { + const filteredEvents = eventQueue.filter(event => + (now - event.timestamp) < MAX_EVENT_AGE + ); + + if (filteredEvents.length !== eventQueue.length) { + this.userEventQueues.set(user_id, filteredEvents); + } + }); + + // Удаляем неактивных пользователей + this.connectedClients.forEach((client, user_id) => { + if ((now - client.lastActivity) > INACTIVE_USER_THRESHOLD) { + this.handleClientDisconnect(user_id); + this.userEventQueues.delete(user_id); + } + }); + } + + // Тестирование Real-time подписки + async testRealtimeConnection() { + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return false; + } + + // Создаем тестовый канал для проверки подключения + const testChannel = supabase + .channel('test_connection') + .subscribe((status, error) => { + if (error) { + console.error('❌ [Supabase] Тестовый канал - ошибка:', error); + } + + if (status === 'SUBSCRIBED') { + // Отписываемся от тестового канала + setTimeout(() => { + testChannel.unsubscribe(); + }, 2000); + } + }); + + return true; + } catch (error) { + console.error('❌ [Supabase] Ошибка тестирования Realtime:', error); + return false; + } + } + + // Проверка статуса подписки + checkSubscriptionStatus() { + if (this.realtimeSubscription) { + return true; + } else { + return false; + } + } + + setupRealtimeSubscription() { + // Убираем setTimeout, вызываем сразу + this._doSetupRealtimeSubscription(); + } + + _doSetupRealtimeSubscription() { + try { + const supabase = getSupabaseClient(); + + if (!supabase) { + console.log('❌ [Supabase] Supabase клиент не найден'); + throw new Error('Supabase client not available'); + } + + // Подписываемся на изменения в таблице messages + const subscription = supabase + .channel('messages_changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const newMessage = payload.new; + if (!newMessage) { + return; + } + + if (!newMessage.chat_id) { + return; + } + + // Получаем профиль пользователя + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', newMessage.user_id) + .single(); + + if (profileError) { + console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); + } + + // Объединяем сообщение с профилем + const messageWithProfile = { + ...newMessage, + user_profiles: userProfile || null + }; + + // Отправляем сообщение всем участникам чата + this.broadcastToChat(newMessage.chat_id, 'new_message', { + message: messageWithProfile, + timestamp: new Date() + }); + + } catch (callbackError) { + console.error('❌ [Supabase] Ошибка в обработчике сообщения:', callbackError); + } + } + ) + .subscribe((status, error) => { + if (error) { + console.error('❌ [Supabase] Ошибка подписки:', error); + } + + if (status === 'CHANNEL_ERROR') { + console.error('❌ [Supabase] Ошибка канала'); + } else if (status === 'TIMED_OUT') { + console.error('❌ [Supabase] Таймаут подписки'); + } + }); + + // Сохраняем ссылку на подписку для возможности отписки + this.realtimeSubscription = subscription; + + } catch (error) { + console.error('❌ [Supabase] Критическая ошибка при настройке подписки:', error); + throw error; // Пробрасываем ошибку для обработки в initializeWithRetry + } + } + + // Получение статистики подключений + getConnectionStats() { + return { + connectedClients: this.connectedClients.size, + activeChats: this.chatParticipants.size, + totalChatParticipants: Array.from(this.chatParticipants.values()) + .reduce((total, participants) => total + participants.size, 0), + totalEventQueues: this.userEventQueues.size, + totalEvents: Array.from(this.userEventQueues.values()) + .reduce((total, queue) => total + queue.length, 0) + }; + } +} + +// Функция для создания роутера с polling эндпоинтами +function createChatPollingRouter(express) { + const router = express.Router(); + const chatHandler = new ChatPollingHandler(); + + // CORS middleware для всех запросов + router.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Authorization'); + res.header('Access-Control-Allow-Credentials', 'true'); + + // Обрабатываем OPTIONS запросы + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + next(); + }); + + // Эндпоинт для аутентификации + router.post('/auth', (req, res) => { + chatHandler.handleAuthentication(req, res); + }); + + // Эндпоинт для получения событий (polling) + router.get('/events', (req, res) => { + chatHandler.handleGetEvents(req, res); + }); + + // HTTP эндпоинты для действий + router.post('/join-chat', (req, res) => { + chatHandler.handleJoinChat(req, res); + }); + + router.post('/leave-chat', (req, res) => { + chatHandler.handleLeaveChat(req, res); + }); + + router.post('/send-message', (req, res) => { + chatHandler.handleSendMessage(req, res); + }); + + router.post('/typing-start', (req, res) => { + chatHandler.handleTypingStart(req, res); + }); + + router.post('/typing-stop', (req, res) => { + chatHandler.handleTypingStop(req, res); + }); + + // Эндпоинт для получения онлайн пользователей в чате + router.get('/online-users/:chat_id', (req, res) => { + const { chat_id } = req.params; + const onlineUsers = chatHandler.getOnlineUsersInChat(chat_id); + res.json({ onlineUsers }); + }); + + // Эндпоинт для получения статистики + router.get('/stats', (req, res) => { + const stats = chatHandler.getConnectionStats(); + res.json(stats); + }); + + // Эндпоинт для проверки статуса Supabase подписки + router.get('/supabase-status', (req, res) => { + const isConnected = chatHandler.checkSubscriptionStatus(); + res.json({ + supabaseSubscriptionActive: isConnected, + subscriptionExists: !!chatHandler.realtimeSubscription, + subscriptionInfo: chatHandler.realtimeSubscription ? { + channel: chatHandler.realtimeSubscription.topic, + state: chatHandler.realtimeSubscription.state + } : null + }); + }); + + // Эндпоинт для принудительного переподключения к Supabase + router.post('/reconnect-supabase', (req, res) => { + try { + // Отписываемся от текущей подписки + if (chatHandler.realtimeSubscription) { + chatHandler.realtimeSubscription.unsubscribe(); + chatHandler.realtimeSubscription = null; + } + + // Создаем новую подписку + chatHandler.setupRealtimeSubscription(); + + res.json({ + success: true, + message: 'Reconnection initiated' + }); + } catch (error) { + console.error('❌ [Polling Server] Ошибка переподключения:', error); + res.status(500).json({ + success: false, + error: 'Reconnection failed', + details: error.message + }); + } + }); + + // Тестовый эндпоинт для создания сообщения в обход API + router.post('/test-message', async (req, res) => { + const { chat_id, user_id, text } = req.body; + + if (!chat_id || !user_id || !text) { + res.status(400).json({ error: 'chat_id, user_id и text обязательны' }); + return; + } + + try { + // Создаем тестовое событие напрямую + chatHandler.broadcastToChat(chat_id, 'new_message', { + message: { + id: `test_${Date.now()}`, + chat_id, + user_id, + text, + created_at: new Date().toISOString(), + user_profiles: { + id: user_id, + full_name: 'Test User', + avatar_url: null + } + }, + timestamp: new Date() + }); + + res.json({ + success: true, + message: 'Test message sent to polling clients' + }); + } catch (error) { + console.error('❌ [Polling Server] Ошибка отправки тестового сообщения:', error); + res.status(500).json({ + success: false, + error: 'Failed to send test message', + details: error.message + }); + } + }); + + return { router, chatHandler }; +} + +module.exports = { + ChatPollingHandler, + createChatPollingRouter +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js deleted file mode 100644 index 62102c5..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js +++ /dev/null @@ -1,457 +0,0 @@ -const { getSupabaseClient } = require('./supabaseClient'); - -class ChatSocketHandler { - constructor(io) { - this.io = io; - this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info - this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) - this.realtimeSubscription = null; // Ссылка на подписку для управления - - this.setupSocketHandlers(); - - try { - this.setupRealtimeSubscription(); // Добавляем Real-time подписки - } catch (error) { - // Ignore error - } - - // Запускаем тестирование через 2 секунды после инициализации - setTimeout(() => { - this.testRealtimeConnection(); - }, 2000); - - // Проверяем статус подписки через 5 секунд - setTimeout(() => { - this.checkSubscriptionStatus(); - }, 5000); - } - - setupSocketHandlers() { - this.io.on('connection', (socket) => { - // Аутентификация пользователя - socket.on('authenticate', async (data) => { - await this.handleAuthentication(socket, data); - }); - - // Присоединение к чату - socket.on('join_chat', async (data) => { - await this.handleJoinChat(socket, data); - }); - - // Покидание чата - socket.on('leave_chat', (data) => { - this.handleLeaveChat(socket, data); - }); - - // Отправка сообщения - socket.on('send_message', async (data) => { - await this.handleSendMessage(socket, data); - }); - - // Пользователь начал печатать - socket.on('typing_start', (data) => { - this.handleTypingStart(socket, data); - }); - - // Пользователь закончил печатать - socket.on('typing_stop', (data) => { - this.handleTypingStop(socket, data); - }); - - // Отключение пользователя - socket.on('disconnect', () => { - this.handleDisconnect(socket); - }); - }); - } - - async handleAuthentication(socket, data) { - try { - const { user_id, token } = data; - - if (!user_id) { - socket.emit('auth_error', { message: 'user_id is required' }); - return; - } - - // Получаем информацию о пользователе из базы данных - const supabase = getSupabaseClient(); - const { data: userProfile, error } = await supabase - .from('user_profiles') - .select('*') - .eq('id', user_id) - .single(); - - if (error) { - socket.emit('auth_error', { message: 'User not found' }); - return; - } - - // Сохраняем информацию о пользователе - this.onlineUsers.set(socket.id, { - user_id, - socket_id: socket.id, - profile: userProfile, - last_seen: new Date() - }); - - socket.user_id = user_id; - socket.emit('authenticated', { - message: 'Successfully authenticated', - user: userProfile - }); - } catch (error) { - socket.emit('auth_error', { message: 'Authentication failed' }); - } - } - - async handleJoinChat(socket, data) { - try { - const { chat_id } = data; - - if (!socket.user_id) { - socket.emit('error', { message: 'Not authenticated' }); - return; - } - - if (!chat_id) { - socket.emit('error', { message: 'chat_id is required' }); - return; - } - // Проверяем, что чат существует и пользователь имеет доступ к нему - const supabase = getSupabaseClient(); - const { data: chat, error } = await supabase - .from('chats') - .select(` - *, - buildings ( - management_company_id, - apartments ( - apartment_residents ( - user_id - ) - ) - ) - `) - .eq('id', chat_id) - .single(); - - if (error || !chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Проверяем доступ пользователя к чату через квартиры в доме - const hasAccess = chat.buildings.apartments.some(apartment => - apartment.apartment_residents.some(resident => - resident.user_id === socket.user_id - ) - ); - - if (!hasAccess) { - socket.emit('error', { message: 'Access denied to this chat' }); - return; - } - // Добавляем сокет в комнату - socket.join(chat_id); - - // Обновляем список участников комнаты - if (!this.chatRooms.has(chat_id)) { - this.chatRooms.set(chat_id, new Set()); - } - - const participantsBefore = this.chatRooms.get(chat_id).size; - this.chatRooms.get(chat_id).add(socket.id); - const participantsAfter = this.chatRooms.get(chat_id).size; - - socket.emit('joined_chat', { - chat_id, - chat: chat, - message: 'Successfully joined chat' - }); - - // Уведомляем других участников о подключении - const userInfo = this.onlineUsers.get(socket.id); - - socket.to(chat_id).emit('user_joined', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } catch (error) { - socket.emit('error', { message: 'Failed to join chat' }); - } - } - - handleLeaveChat(socket, data) { - const { chat_id } = data; - - if (!chat_id) return; - - socket.leave(chat_id); - - // Удаляем из списка участников - if (this.chatRooms.has(chat_id)) { - this.chatRooms.get(chat_id).delete(socket.id); - - // Если комната пуста, удаляем её - if (this.chatRooms.get(chat_id).size === 0) { - this.chatRooms.delete(chat_id); - } - } - - // Уведомляем других участников об отключении - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_left', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - - - } - - async handleSendMessage(socket, data) { - try { - const { chat_id, text } = data; - - if (!socket.user_id) { - socket.emit('error', { message: 'Not authenticated' }); - return; - } - - if (!chat_id || !text) { - socket.emit('error', { message: 'chat_id and text are required' }); - return; - } - - // Сохраняем сообщение в базу данных - const supabase = getSupabaseClient(); - const { data: message, error } = await supabase - .from('messages') - .insert({ - chat_id, - user_id: socket.user_id, - text - }) - .select(` - *, - user_profiles ( - id, - full_name, - avatar_url - ) - `) - .single(); - - if (error) { - socket.emit('error', { message: 'Failed to save message' }); - return; - } - - // Отправляем сообщение всем участникам чата - this.io.to(chat_id).emit('new_message', { - message, - timestamp: new Date() - }); - - } catch (error) { - socket.emit('error', { message: 'Failed to send message' }); - } - } - - handleTypingStart(socket, data) { - const { chat_id } = data; - - if (!socket.user_id || !chat_id) return; - - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_typing_start', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } - - handleTypingStop(socket, data) { - const { chat_id } = data; - - if (!socket.user_id || !chat_id) return; - - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_typing_stop', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } - - handleDisconnect(socket) { - - // Удаляем пользователя из всех комнат - this.chatRooms.forEach((participants, chat_id) => { - if (participants.has(socket.id)) { - participants.delete(socket.id); - - // Уведомляем других участников об отключении - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_left', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - - // Если комната пуста, удаляем её - if (participants.size === 0) { - this.chatRooms.delete(chat_id); - } - } - }); - - // Удаляем пользователя из списка онлайн - this.onlineUsers.delete(socket.id); - } - - // Получение списка онлайн пользователей в чате - getOnlineUsersInChat(chat_id) { - const participants = this.chatRooms.get(chat_id) || new Set(); - const onlineUsers = []; - - participants.forEach(socketId => { - const userInfo = this.onlineUsers.get(socketId); - if (userInfo) { - onlineUsers.push(userInfo.profile); - } - }); - - return onlineUsers; - } - - // Отправка системного сообщения в чат - async sendSystemMessage(chat_id, text) { - this.io.to(chat_id).emit('system_message', { - chat_id, - text, - timestamp: new Date() - }); - } - - // Тестирование Real-time подписки - async testRealtimeConnection() { - try { - const supabase = getSupabaseClient(); - if (!supabase) { - return false; - } - - // Создаем тестовый канал для проверки подключения - const testChannel = supabase - .channel('test_connection') - .subscribe((status, error) => { - if (status === 'SUBSCRIBED') { - // Отписываемся от тестового канала - setTimeout(() => { - testChannel.unsubscribe(); - }, 2000); - } - }); - - return true; - } catch (error) { - return false; - } - } - - // Проверка статуса подписки - checkSubscriptionStatus() { - if (this.realtimeSubscription) { - return true; - } else { - return false; - } - } - - setupRealtimeSubscription() { - // Добавляем небольшую задержку, чтобы убедиться, что Supabase клиент инициализирован - setTimeout(() => { - this._doSetupRealtimeSubscription(); - }, 1000); - } - - _doSetupRealtimeSubscription() { - try { - const supabase = getSupabaseClient(); - - if (!supabase) { - return; - } - - // Подписываемся на изменения в таблице messages - const subscription = supabase - .channel('messages_changes') - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'messages' - }, - async (payload) => { - try { - const newMessage = payload.new; - if (!newMessage) { - return; - } - - if (!newMessage.chat_id) { - return; - } - - // Получаем профиль пользователя - const { data: userProfile, error: profileError } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', newMessage.user_id) - .single(); - - // Объединяем сообщение с профилем - const messageWithProfile = { - ...newMessage, - user_profiles: userProfile || null - }; - - // Проверяем, есть ли участники в чате - const chatRoomParticipants = this.chatRooms.get(newMessage.chat_id); - - // Отправляем сообщение через Socket.IO всем участникам чата - this.io.to(newMessage.chat_id).emit('new_message', { - message: messageWithProfile, - timestamp: new Date() - }); - } catch (callbackError) { - // Ignore error - } - } - ) - .subscribe(); - - // Сохраняем ссылку на подписку для возможности отписки - this.realtimeSubscription = subscription; - - } catch (error) { - // Ignore error - } - } -} - -// Функция инициализации Socket.IO для чатов -function initializeChatSocket(io) { - const chatHandler = new ChatSocketHandler(io); - - return chatHandler; -} - -module.exports = { - ChatSocketHandler, - initializeChatSocket -}; \ No newline at end of file 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 938cc18..0568afa 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -3,12 +3,30 @@ const { createClient } = require('@supabase/supabase-js'); const { getSupabaseUrl, getSupabaseKey, getSupabaseServiceKey } = require('./get-constants'); let supabase = null; +let initializationPromise = null; async function initSupabaseClient() { - const supabaseUrl = await getSupabaseUrl(); - const supabaseAnonKey = await getSupabaseKey(); - const supabaseServiceRoleKey = await getSupabaseServiceKey(); - supabase = createClient(supabaseUrl, supabaseServiceRoleKey); + console.log('🔄 [Supabase Client] Начинаем инициализацию...'); + + try { + console.log('🔄 [Supabase Client] Получаем конфигурацию...'); + const supabaseUrl = await getSupabaseUrl(); + const supabaseAnonKey = await getSupabaseKey(); + const supabaseServiceRoleKey = await getSupabaseServiceKey(); + + + if (!supabaseUrl || !supabaseServiceRoleKey) { + throw new Error('Missing required Supabase configuration'); + } + + supabase = createClient(supabaseUrl, supabaseServiceRoleKey); + + return supabase; + + } catch (error) { + console.error('❌ [Supabase Client] Ошибка инициализации:', error); + throw error; + } } function getSupabaseClient() { @@ -20,20 +38,49 @@ function getSupabaseClient() { // POST /refresh-supabase-client router.post('/refresh-supabase-client', async (req, res) => { -try { + try { await initSupabaseClient(); res.json({ success: true, message: 'Supabase client refreshed' }); -} catch (error) { + } catch (error) { + console.error('❌ [Supabase Client] Ошибка обновления:', error); res.status(500).json({ error: error.message }); -} + } +}); + +// GET /supabase-client-status +router.get('/supabase-client-status', (req, res) => { + console.log('🔍 [Supabase Client] Проверяем статус клиента...'); + + const isInitialized = !!supabase; + + res.json({ + initialized: isInitialized, + clientExists: !!supabase, + timestamp: new Date().toISOString() + }); }); // Инициализация клиента при старте -(async () => { +initializationPromise = (async () => { + try { await initSupabaseClient(); + } catch (error) { + console.error('❌ [Supabase Client] Ошибка инициализации при старте:', error); + // Планируем повторную попытку через 5 секунд + setTimeout(async () => { + try { + await initSupabaseClient(); + } catch (retryError) { + console.error('❌ [Supabase Client] Повторная инициализация неудачна:', retryError); + } + }, 5000); + } })(); module.exports = { getSupabaseClient, - supabaseRouter: router + initSupabaseClient, + supabaseRouter: router, + // Экспортируем промис инициализации для возможности ожидания + initializationPromise }; \ No newline at end of file From 37238a13858664c7ffe34adb1f7afe6e507fc56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Sun, 15 Jun 2025 16:13:57 +0300 Subject: [PATCH 09/10] change moderate and initiatives --- .../sber_mobile/initiatives-ai-agents/llm.ts | 22 +++ .../initiatives-ai-agents/moderation.ts | 33 ++--- .../initiatives-ai-agents/picture.ts | 21 +-- .../kfu-m-24-1/sber_mobile/initiatives.js | 4 +- .../kfu-m-24-1/sber_mobile/moderate.js | 128 ++++++++++++------ 5 files changed, 125 insertions(+), 83 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts new file mode 100644 index 0000000..a5a0e23 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts @@ -0,0 +1,22 @@ +import { GigaChat as GigaChatLang} from 'langchain-gigachat'; +import { GigaChat } from 'gigachat'; +import { Agent } from 'node:https'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +export const llm_mod = (GIGA_AUTH) => + new GigaChatLang({ + credentials: GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2-Max', + httpsAgent, +}); + +export const llm_gen = (GIGA_AUTH) => + new GigaChat({ + credentials: GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts index 7a34fdb..dc2022c 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -1,32 +1,23 @@ -import { Agent } from 'node:https'; -import { GigaChat } from "langchain-gigachat"; +import { llm_mod } from './llm' import { z } from "zod"; -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); - -const llm = new GigaChat({ - credentials: process.env.GIGA_AUTH, - temperature: 0.2, - model: 'GigaChat-2', - httpsAgent, -}); // возвращаю комментарий + исправленное предложение + булево значение -const moderationLlm = llm.withStructuredOutput(z.object({ - comment: z.string(), - fixedText: z.string().optional(), - isApproved: z.boolean(), -}) as any) -export const moderationText = async (title: string, body: string): Promise<[string, string | undefined, boolean]> => { +export const moderationText = async (title: string, description: string, GIGA_AUTH): Promise<[string, string | undefined, boolean]> => { + + const moderationLlm = llm_mod(GIGA_AUTH).withStructuredOutput(z.object({ + comment: z.string(), + fixedText: z.string().optional(), + isApproved: z.boolean(), + }) as any) + const prompt = ` Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, не имеющая отношения к Управляющей компании). Заголовок: ${title} - Основной текст: ${body} + Основной текст: ${description} Твои задачи: 1. Проверь предложение и заголовок на спам. @@ -58,9 +49,9 @@ export const moderationText = async (title: string, body: string): Promise<[stri const result = await moderationLlm.invoke(prompt); console.log(result) // Дополнительная проверка - if(!result.isApproved && result.comment.trim() === '' && result.fixedText.trim() === '') { + if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) { result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', - result.fixedText = body + result.fixedText = description } return [result.comment, result.fixedText, result.isApproved]; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts index 2544dd3..d216c5d 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -1,19 +1,8 @@ -import { GigaChat, detectImage } from 'gigachat'; -import { Agent } from 'node:https'; +import { llm_gen } from './llm' +import { detectImage } from 'gigachat'; -const httpsAgent = new Agent({ - rejectUnauthorized: false, - timeout: 60000 -}); - -export const llm = new GigaChat({ - credentials: process.env.GIGA_AUTH, - model: 'GigaChat-2', - httpsAgent, -}); - -export const generatePicture = async (prompt: string) => { - const resp = await llm.chat({ +export const generatePicture = async (prompt: string, GIGA_AUTH) => { + const resp = await llm_gen(GIGA_AUTH).chat({ messages: [ { "role": "system", @@ -36,7 +25,7 @@ export const generatePicture = async (prompt: string) => { throw new Error('Не удалось получить UUID изображения из ответа GigaChat'); } - const image = await llm.getImage(detectedImage.uuid); + const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid); // Возвращаем содержимое изображения, убеждаясь что это Buffer if (Buffer.isBuffer(image.content)) { diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js index 3ca0562..42f82e4 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js @@ -38,9 +38,9 @@ router.get('/initiatives/:id', async (req, res) => { // Создать инициативу router.post('/initiatives', async (req, res) => { const supabase = getSupabaseClient(); - const { building_id, creator_id, title, description, status, target_amount, image_url } = req.body; + const { building_id, creator_id, title, description, status, target_amount, current_amount, image_url } = req.body; const { data, error } = await supabase.from('initiatives').insert([ - { building_id, creator_id, title, description, status, target_amount, image_url } + { building_id, creator_id, title, description, status, target_amount, current_amount: current_amount || 0, image_url } ]).select().single(); if (error) return res.status(400).json({ error: error.message }); res.json(data); diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js index 25b5e3a..0e8b3f8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.js +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -2,63 +2,76 @@ const router = require('express').Router(); const { moderationText } = require('./initiatives-ai-agents/moderation.ts'); const { generatePicture } = require('./initiatives-ai-agents/picture.ts'); const { getSupabaseClient } = require('./supabaseClient'); +const { getGigaAuth } = require('./get-constants'); -// Обработчик для модерации текста +async function getGigaKey() { + const GIGA_AUTH = await getGigaAuth(); + return GIGA_AUTH; + } + +// Обработчик для модерации и создания инициативы router.post('/moderate', async (req, res) => { + + const GIGA_AUTH = await getGigaKey(); + try { - const { title, body } = req.body; - if (!title || !body) { - res.status(400).json({ error: 'Заголовок и текст обязательны' }); + const { title, description, building_id, creator_id, target_amount, status } = req.body; + + if (!title || !description) { + res.status(400).json({ error: 'Заголовок и описание обязательны' }); return; } - console.log('Запрос на модерацию:', { title: title.substring(0, 50), body: body.substring(0, 100) }); + if (!building_id || !creator_id) { + res.status(400).json({ error: 'ID дома и создателя обязательны' }); + return; + } - const [comment, fixedText, isApproved] = await moderationText(title, body); + // Валидация статуса, если передан + const validStatuses = ['moderation', 'review', 'fundraising', 'approved', 'rejected']; + if (status && !validStatuses.includes(status)) { + res.status(400).json({ error: `Недопустимый статус. Допустимые значения: ${validStatuses.join(', ')}` }); + return; + } + + console.log('Запрос на модерацию:', { title: title.substring(0, 50), description: description.substring(0, 100) }); + + // Модерация текста (передаем title и description как body) + const [comment, fixedText, isApproved] = await moderationText(title, description, GIGA_AUTH); console.log('Результат модерации получен:', { comment, fixedText: fixedText?.substring(0, 100), isApproved }); - // Дополнительная проверка на стороне сервера - if (!isApproved && (!comment || comment.trim() === '')) { - console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении'); - } - - res.json({ - comment, - fixedText, - isApproved - }); - } catch (error) { - console.error('Error in moderation:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); - } -}); - -// Обработчик для генерации изображений -router.post('/generate-image', async (req, res) => { - try { - const { prompt, userId } = req.body; - if (!prompt) { - res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); + // Если модерация не прошла, возвращаем undefined + if (!isApproved) { + if (!comment || comment.trim() === '') { + console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении'); + } + + res.json({ + comment, + fixedText, + isApproved, + initiative: undefined + }); return; } - - // Генерируем изображение - const imageBuffer = await generatePicture(prompt); - //console.log('Изображение получено, размер буфера:', imageBuffer?.length || 0, 'байт'); + // Модерация прошла, генерируем изображение используя заголовок как промпт + console.log('Модерация прошла, генерируем изображение с промптом:', title); + + const imageBuffer = await generatePicture(title, GIGA_AUTH); + if (!imageBuffer || imageBuffer.length === 0) { res.status(500).json({ error: 'Получен пустой буфер изображения' }); return; } - //console.log('Начинаем загрузку в Supabase Storage...'); - // Получаем Supabase клиент и создаем имя файла const supabase = getSupabaseClient(); const timestamp = Date.now(); - const filename = `image_${userId || 'user'}_${timestamp}.jpg`; + const filename = `image_${creator_id}_${timestamp}.jpg`; + // Загружаем изображение в Supabase Storage let uploadResult; let retries = 0; const maxRetries = 5; @@ -76,7 +89,6 @@ router.post('/generate-image', async (req, res) => { break; // Успешная загрузка } - //console.warn(`Попытка загрузки ${retries + 1} неудачна:`, uploadResult.error); retries++; if (retries < maxRetries) { @@ -84,7 +96,7 @@ router.post('/generate-image', async (req, res) => { await new Promise(resolve => setTimeout(resolve, 1000 * retries)); } } catch (error) { - //console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message); + console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message); retries++; if (retries < maxRetries) { @@ -97,26 +109,54 @@ router.post('/generate-image', async (req, res) => { } if (uploadResult?.error) { - //console.error('Supabase storage error after all retries:', uploadResult.error); + console.error('Supabase storage error after all retries:', uploadResult.error); res.status(500).json({ error: 'Ошибка при сохранении изображения после нескольких попыток' }); return; } - //console.log('Изображение успешно загружено в Supabase Storage:', filename); + console.log('Изображение успешно загружено в Supabase Storage:', filename); // Получаем публичный URL const { data: urlData } = supabase.storage .from('images') .getPublicUrl(filename); - + + // Определяем статус: если передан в запросе, используем его, иначе 'review' + const finalStatus = status || 'review'; + + // Создаем инициативу в базе данных + const { data: initiative, error: initiativeError } = await supabase + .from('initiatives') + .insert([{ + building_id, + creator_id, + title: fixedText || title, + description, + status: finalStatus, + target_amount: target_amount || null, + current_amount: 0, + image_url: urlData.publicUrl + }]) + .select() + .single(); + + if (initiativeError) { + console.error('Ошибка создания инициативы:', initiativeError); + res.status(500).json({ error: 'Ошибка при создании инициативы', details: initiativeError.message }); + return; + } + + console.log('Инициатива успешно создана:', initiative.id); + res.json({ - success: true, - imageUrl: urlData.publicUrl, - imagePath: filename + comment, + fixedText, + isApproved, + initiative }); } catch (error) { - //console.error('Error in image generation:', error); + console.error('Error in moderation and initiative creation:', error); res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); } }); From cc2a66367dff2fc803aade9003c1aa1acee7a37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Sun, 15 Jun 2025 20:39:27 +0300 Subject: [PATCH 10/10] votes --- .../routers/kfu-m-24-1/sber_mobile/votes.js | 95 +++++++++++++++---- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/votes.js b/server/routers/kfu-m-24-1/sber_mobile/votes.js index d46df22..9a861da 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/votes.js +++ b/server/routers/kfu-m-24-1/sber_mobile/votes.js @@ -6,39 +6,100 @@ router.get('/votes/:initiative_id', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id } = req.params; const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); - if (error) return res.status(400).json({ error: error.message }); + if (error) + return res.status(400).json({ error: error.message }); res.json(data); }); // Получить голос пользователя по инициативе -router.get('/votes/:initiative_id/:user_id', async (req, res) => { +router.get('/votes/:initiative_id/user/:user_id', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id, user_id } = req.params; const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id).eq('user_id', user_id).single(); - if (error) return res.status(400).json({ error: error.message }); + if (error) { + console.log(error, '/votes/:initiative_id/:user_id') + console.log(initiative_id, user_id) + return res.status(400).json({ error: error.message }); + } res.json(data); }); -// Получить все голоса по инициативе (через query) -router.get('/votes', async (req, res) => { +// Получить статистику голосов по инициативе +router.get('/votes/stats/:initiative_id', async (req, res) => { const supabase = getSupabaseClient(); - const { initiative_id } = req.query; - if (!initiative_id) return res.status(400).json({ error: 'initiative_id required' }); - const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + const { initiative_id } = req.params; + + const { data, error } = await supabase + .from('votes') + .select('vote_type') + .eq('initiative_id', initiative_id); + console.log(data, error) + if (error) { + console.log('/votes/:initiative_id/stats') + res.status(400).json({ error: error.message }); + } + const stats = { + for: data.filter(vote => vote.vote_type === 'for').length, + against: data.filter(vote => vote.vote_type === 'against').length, + total: data.length + }; + + res.json(stats); }); -// Проголосовать (создать или обновить голос) +// Проголосовать (создать, обновить или удалить голос) router.post('/votes', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id, user_id, vote_type } = req.body; - // upsert: если голос уже есть, обновить, иначе создать - const { data, error } = await supabase.from('votes').upsert([ - { initiative_id, user_id, vote_type } - ], { onConflict: ['initiative_id', 'user_id'] }).select().single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + + // Проверяем существующий голос + const { data: existingVote, error: checkError } = await supabase + .from('votes') + .select('*') + .eq('initiative_id', initiative_id) + .eq('user_id', user_id) + .single(); + + if (checkError && checkError.code !== 'PGRST116') { + console.log('1/votes') + return res.status(400).json({ error: checkError.message }); + } + + if (existingVote) { + if (existingVote.vote_type === vote_type) { + // Если нажали тот же тип голоса - УДАЛЯЕМ (отменяем голос) + const { error: deleteError } = await supabase + .from('votes') + .delete() + .eq('initiative_id', initiative_id) + .eq('user_id', user_id); + + if (deleteError) return res.status(400).json({ error: deleteError.message }); + res.json({ message: 'Vote removed', action: 'removed', previous_vote: existingVote.vote_type }); + } else { + // Если нажали другой тип голоса - ОБНОВЛЯЕМ + const { data, error } = await supabase + .from('votes') + .update({ vote_type }) + .eq('initiative_id', initiative_id) + .eq('user_id', user_id) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ ...data, action: 'updated', previous_vote: existingVote.vote_type }); + } + } else { + // Если голоса нет - СОЗДАЕМ новый + const { data, error } = await supabase + .from('votes') + .insert([{ initiative_id, user_id, vote_type }]) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ ...data, action: 'created' }); + } }); module.exports = router; \ No newline at end of file