Запрос на слияние 'sber_mobile' (#41) из sber_mobile в main
This commit is contained in:
@@ -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);
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
164
server/routers/kfu-m-24-1/sber_mobile/moderate.js
Normal file
164
server/routers/kfu-m-24-1/sber_mobile/moderate.js
Normal 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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user