Запрос на слияние 'sber_mobile' (#41) из sber_mobile в main

This commit is contained in:
DmitrievMS
2025-06-15 19:54:47 +00:00
7 changed files with 364 additions and 32 deletions

View File

@@ -1,6 +1,4 @@
const router = require('express').Router();
const authRouter = require('./auth');
const { supabaseRouter } = require('./supabaseClient');
const profileRouter = require('./profile');
@@ -10,22 +8,19 @@ const additionalServicesRouter = require('./additional_services');
const chatsRouter = require('./chats');
const camerasRouter = require('./cameras');
const ticketsRouter = require('./tickets');
const messagesRouter = require('./messages');
const moderationRouter = require('./moderation');
const utilityPaymentsRouter = require('./utility_payments');
const apartmentsRouter = require('./apartments');
const buildingsRouter = require('./buildings');
const userApartmentsRouter = require('./user_apartments');
const avatarRouter = require('./media');
const supportRouter = require('./supportApi');
const moderateRouter = require('./moderate.js');
module.exports = router;
router.use('/auth', authRouter);
router.use('/supabase', supabaseRouter);
router.use('', profileRouter);
@@ -35,18 +30,12 @@ router.use('', additionalServicesRouter);
router.use('', chatsRouter);
router.use('', camerasRouter);
router.use('', ticketsRouter);
router.use('', messagesRouter);
router.use('', moderationRouter);
router.use('', utilityPaymentsRouter);
router.use('', apartmentsRouter);
router.use('', buildingsRouter);
router.use('', userApartmentsRouter);
router.use('', avatarRouter);
router.use('', supportRouter);
router.use('', moderateRouter);

View File

@@ -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,
});

View File

@@ -0,0 +1,58 @@
import { llm_mod } from './llm'
import { z } from "zod";
// возвращаю комментарий + исправленное предложение + булево значение
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}
Основной текст: ${description}
Твои задачи:
1. Проверь предложение и заголовок на спам.
2. Проверь, чтобы заголовок и текст были на одну тему.
3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей.
4. Проверь грамматику.
5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы.
6. Не должно быть рекламы, ссылок и т.д.
7. Проверь предложение на информативность, предложение не может быть коротким, оно должно ясно отражжать суть инициативы.
8. Предложение должно быть в вежливой форме.
- Если все правила соблюдены, то предложение принимается!
- Если предложение отклонено, всегда пиши комментарий и fixedText!
Правила написания комментария:
- Если предложение отклоняется, пиши комментарий со следующей формулировкой:
"Предложение отклонено. Причина: (укажи проблему)"
Правила написания fixedText:
- Если предложение отклонено, то верни в поле "fixedText" измененный текст, который будет соответствовать правилам.
- Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию,
которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл.
- Если текст не представляет никакой ценности, возврати в поле "fixedText" правило,
по которому оно не прошло.
-Если предложение принимается, то ничего не возвращай в поле fixedText.
`
const result = await moderationLlm.invoke(prompt);
console.log(result)
// Дополнительная проверка
if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) {
result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.',
result.fixedText = description
}
return [result.comment, result.fixedText, result.isApproved];
};

View File

@@ -0,0 +1,38 @@
import { llm_gen } from './llm'
import { detectImage } from 'gigachat';
export const generatePicture = async (prompt: string, GIGA_AUTH) => {
const resp = await llm_gen(GIGA_AUTH).chat({
messages: [
{
"role": "system",
"content": "Ты — Василий Кандинский для жильцов многоквартирного дома"
},
{
role: "user",
content: `Старайся передать атмосферу уюта и безопасности.
Нарисуй картинку подходящую для такого события: ${prompt}
В картинке не должно быть текста, только изображение.`,
},
],
function_call: 'auto',
});
// Получение изображения по идентификатору
const detectedImage = detectImage(resp.choices[0]?.message.content ?? '');
if (!detectedImage?.uuid) {
throw new Error('Не удалось получить UUID изображения из ответа GigaChat');
}
const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid);
// Возвращаем содержимое изображения, убеждаясь что это 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);
}
}

View File

@@ -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);

View File

@@ -0,0 +1,164 @@
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, description, building_id, creator_id, target_amount, status } = req.body;
if (!title || !description) {
res.status(400).json({ error: 'Заголовок и описание обязательны' });
return;
}
if (!building_id || !creator_id) {
res.status(400).json({ error: 'ID дома и создателя обязательны' });
return;
}
// Валидация статуса, если передан
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 });
// Если модерация не прошла, возвращаем undefined
if (!isApproved) {
if (!comment || comment.trim() === '') {
console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении');
}
res.json({
comment,
fixedText,
isApproved,
initiative: undefined
});
return;
}
// Модерация прошла, генерируем изображение используя заголовок как промпт
console.log('Модерация прошла, генерируем изображение с промптом:', title);
const imageBuffer = await generatePicture(title, GIGA_AUTH);
if (!imageBuffer || imageBuffer.length === 0) {
res.status(500).json({ error: 'Получен пустой буфер изображения' });
return;
}
// Получаем Supabase клиент и создаем имя файла
const supabase = getSupabaseClient();
const timestamp = Date.now();
const filename = `image_${creator_id}_${timestamp}.jpg`;
// Загружаем изображение в Supabase Storage
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; // Успешная загрузка
}
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);
// Определяем статус: если передан в запросе, используем его, иначе '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({
comment,
fixedText,
isApproved,
initiative
});
} catch (error) {
console.error('Error in moderation and initiative creation:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message });
}
});
module.exports = router;

View File

@@ -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;