feat: новая функция

This commit is contained in:
2025-11-01 17:25:49 +03:00
parent 772d4cff54
commit e28848146d
19 changed files with 1680 additions and 67 deletions

View File

@@ -330,11 +330,75 @@ class UniversalGuestService {
logger.info(`[UniversalGuestService] Обработка сообщения гостя: ${identifier}`);
// 0.5. Проверяем, нужно ли автоматически подписать согласие при ответе
// Загружаем историю для проверки последнего сообщения
const previousHistory = await this.getHistory(identifier);
// Если в истории есть системное сообщение о согласиях, автоматически подписываем при ответе
if (previousHistory.length > 0) {
const consentService = require('./consentService');
const [provider, providerId] = identifier?.split(':') || [];
let walletAddress = null;
if (provider === 'web' && providerId?.startsWith('guest_')) {
walletAddress = providerId;
}
// Проверяем, было ли последнее сообщение системным с согласием
const lastMessage = previousHistory[previousHistory.length - 1];
const hasConsentSystemMessage = lastMessage &&
(lastMessage.role === 'system' || lastMessage.consentRequired);
if (hasConsentSystemMessage) {
// Проверяем текущие согласия
const consentCheck = await consentService.checkConsents({
userId: null,
walletAddress
});
// Если согласия нужны, автоматически подписываем
if (consentCheck.needsConsent) {
logger.info(`[UniversalGuestService] Автоматическое подписание согласий при ответе гостя ${identifier}`);
const consentDocuments = await consentService.getConsentDocuments(consentCheck.missingConsents);
const documentIds = consentDocuments.map(doc => doc.id);
const consentTypes = consentDocuments.map(doc => doc.consentType).filter(type => type);
if (documentIds.length > 0 && consentTypes.length > 0) {
const db = require('../db');
try {
// Для гостей используем wallet_address в формате guest_ID
await db.getQuery()(
`INSERT INTO consent_logs (wallet_address, document_id, document_title, consent_type, status, signed_at, channel, ip_address, created_at, updated_at)
SELECT $1, unnest($2::int[]), unnest($3::text[]), unnest($4::text[]), 'granted', NOW(), 'web', NULL, NOW(), NOW()
ON CONFLICT (wallet_address, consent_type, document_id)
DO UPDATE SET
status = 'granted',
signed_at = NOW(),
revoked_at = NULL,
updated_at = NOW()
WHERE consent_logs.wallet_address = $1 AND consent_logs.consent_type = EXCLUDED.consent_type`,
[
walletAddress,
documentIds,
consentDocuments.map(doc => doc.title),
consentTypes
]
);
logger.info(`[UniversalGuestService] Согласия автоматически подписаны для гостя ${identifier}`);
} catch (consentError) {
logger.error(`[UniversalGuestService] Ошибка автоматического подписания согласий:`, consentError);
}
}
}
}
}
// 1. Сохраняем сообщение гостя
const saveResult = await this.saveMessage(messageData);
const processedContent = saveResult.processedContent;
// 2. Загружаем историю для контекста
// 2. Загружаем историю для контекста (заново, так как могли добавиться сообщения)
const conversationHistory = await this.getHistory(identifier);
// 3. Генерируем AI ответ
@@ -368,25 +432,74 @@ class UniversalGuestService {
};
}
// 4. Сохраняем AI ответ
// Проверяем согласия для добавления системного сообщения к ответу ИИ
const consentService = require('./consentService');
const [provider, providerId] = identifier?.split(':') || [];
let walletAddress = null;
if (provider === 'web' && providerId?.startsWith('guest_')) {
walletAddress = providerId; // Для веб-гостей используем guest_ID
}
const consentCheck = await consentService.checkConsents({
userId: null,
walletAddress
});
// Формируем финальный ответ ИИ с системным сообщением, если нужно
let finalAiResponse = aiResponse.response;
let consentInfo = null;
if (consentCheck.needsConsent) {
const consentSystemMessage = await consentService.getConsentSystemMessage({
userId: null,
walletAddress,
channel: channel === 'web' ? 'web' : channel,
baseUrl: process.env.BASE_URL || 'http://localhost:9000'
});
if (consentSystemMessage && consentSystemMessage.consentRequired) {
// Добавляем системное сообщение к ответу ИИ
finalAiResponse = `${aiResponse.response}\n\n---\n\n${consentSystemMessage.content}`;
consentInfo = {
consentRequired: true,
missingConsents: consentSystemMessage.missingConsents,
consentDocuments: consentSystemMessage.consentDocuments,
autoConsentOnReply: consentSystemMessage.autoConsentOnReply
};
}
}
// 4. Сохраняем AI ответ с добавленным системным сообщением
await this.saveAiResponse({
identifier,
content: aiResponse.response,
content: finalAiResponse,
channel,
metadata: messageData.metadata || {}
});
logger.info(`[UniversalGuestService] Сообщение гостя ${identifier} обработано успешно`);
return {
const result = {
success: true,
identifier,
aiResponse: {
response: aiResponse.response,
response: finalAiResponse,
ragData: aiResponse.ragData
}
};
// Добавляем информацию о согласиях, если они нужны
if (consentInfo) {
result.consentRequired = consentInfo.consentRequired;
result.missingConsents = consentInfo.missingConsents;
result.consentDocuments = consentInfo.consentDocuments;
result.autoConsentOnReply = consentInfo.autoConsentOnReply;
}
return result;
} catch (error) {
logger.error('[UniversalGuestService] Ошибка обработки сообщения гостя:', error);
throw error;
@@ -534,6 +647,48 @@ class UniversalGuestService {
);
}
// 5. Переносим согласия гостя на пользователя, если они есть
// Согласия могут быть связаны с гостевой сессией через wallet_address = "guest_${guestId}"
try {
const [channel, guestId] = identifier.split(':');
// Ищем согласия по гостевому идентификатору в формате "guest_${guestId}"
const guestWalletAddress = `guest_${guestId}`;
const { rows: guestConsents } = await db.getQuery()(`
SELECT id, consent_type, document_id, document_title, status, signed_at, ip_address, user_agent, channel as consent_channel
FROM consent_logs
WHERE wallet_address = $1
AND status = 'granted'
AND (user_id IS NULL OR user_id = $2)
`, [guestWalletAddress, userId]);
if (guestConsents.length > 0) {
logger.info(`[UniversalGuestService] Найдено ${guestConsents.length} согласий для переноса`);
// Переносим согласия на пользователя
// Обновляем wallet_address на нормализованный адрес кошелька пользователя, если он есть
const identityService = require('./identity-service');
const walletIdentity = await identityService.findIdentity(userId, 'wallet');
const normalizedWalletAddress = walletIdentity?.provider_id || null;
for (const consent of guestConsents) {
await db.getQuery()(`
UPDATE consent_logs
SET user_id = $1,
wallet_address = COALESCE($2, wallet_address),
updated_at = NOW()
WHERE id = $3
`, [userId, normalizedWalletAddress, consent.id]);
}
logger.info(`[UniversalGuestService] Перенесено ${guestConsents.length} согласий на user ${userId}`);
}
} catch (consentError) {
// Не критично, если не удалось перенести согласия - просто логируем
logger.warn(`[UniversalGuestService] Ошибка переноса согласий (не критично):`, consentError);
}
logger.info(`[UniversalGuestService] Миграция завершена: ${migrated} перенесено, ${skipped} пропущено`);
return {

View File

@@ -33,28 +33,47 @@ const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
class AuthService {
constructor() {}
// Проверка подписи
async verifySignature(message, signature, address) {
// Проверка подписи SIWE
// Используем SiweMessage для правильной проверки SIWE подписей
async verifySignature(siweMessage, signature, address) {
try {
if (!message || !signature || !address) return false;
if (!siweMessage || !signature || !address) return false;
// Нормализуем входящий адрес
const normalizedAddress = ethers.getAddress(address);
// Восстанавливаем адрес из подписи
const recoveredAddress = ethers.verifyMessage(message, signature);
// Используем SiweMessage.verify() для правильной проверки SIWE подписи
const { SiweMessage } = require('siwe');
// Если siweMessage уже является объектом SiweMessage, используем его напрямую
// Если это строка, парсим её
let message;
if (typeof siweMessage === 'string') {
message = new SiweMessage(siweMessage);
} else {
message = siweMessage;
}
// Проверяем подпись через SiweMessage.verify()
const { success, data } = await message.verify({ signature });
// Логируем для отладки
logger.info(`[verifySignature] Message: ${message}`);
logger.info(`[verifySignature] SIWE verification result: ${success}`);
if (data) {
logger.info(`[verifySignature] Verified address: ${data.address}`);
logger.info(`[verifySignature] Expected address: ${normalizedAddress}`);
logger.info(`[verifySignature] Addresses match: ${ethers.getAddress(data.address) === normalizedAddress}`);
}
logger.info(`[verifySignature] Signature: ${signature}`);
logger.info(`[verifySignature] Expected address: ${normalizedAddress}`);
logger.info(`[verifySignature] Recovered address: ${recoveredAddress}`);
logger.info(`[verifySignature] Addresses match: ${ethers.getAddress(recoveredAddress) === normalizedAddress}`);
if (!success) {
return false;
}
// Сравниваем нормализованные адреса
return ethers.getAddress(recoveredAddress) === normalizedAddress;
return data && ethers.getAddress(data.address) === normalizedAddress;
} catch (error) {
logger.error('Error in signature verification:', error);
logger.error('Error in SIWE signature verification:', error);
return false;
}
}

View File

@@ -0,0 +1,263 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/VC-HB3-Accelerator
*/
const db = require('../db');
const logger = require('../utils/logger');
// Маппинг названий документов на типы согласий
const DOCUMENT_CONSENT_MAP = {
'Политика конфиденциальности': 'privacy_policy',
'Права субъектов персональных данных и отзыв согласия': 'personal_data',
'Согласие на использование файлов cookie': 'cookies',
'Согласие на обработку персональных данных': 'personal_data_processing'
};
/**
* Проверить согласия пользователя или гостя
* @param {Object} params - Параметры проверки
* @param {number|null} params.userId - ID пользователя (если авторизован)
* @param {string|null} params.walletAddress - Адрес кошелька или guest_ID
* @returns {Promise<Object>} - Результат проверки с информацией о недостающих согласиях
*/
async function checkConsents({ userId = null, walletAddress = null }) {
try {
const requiredConsentTypes = Object.values(DOCUMENT_CONSENT_MAP);
// Строим запрос в зависимости от наличия userId
let consentCheckQuery;
let queryParams;
if (userId) {
// Для авторизованного пользователя
consentCheckQuery = `
SELECT consent_type, COUNT(*) as count
FROM consent_logs
WHERE status = 'granted'
AND (user_id = $1 OR wallet_address = $2)
AND consent_type = ANY($3)
GROUP BY consent_type
`;
queryParams = [userId, walletAddress, requiredConsentTypes];
} else if (walletAddress) {
// Для гостя
consentCheckQuery = `
SELECT consent_type, COUNT(*) as count
FROM consent_logs
WHERE wallet_address = $1
AND status = 'granted'
AND consent_type = ANY($2)
GROUP BY consent_type
`;
queryParams = [walletAddress, requiredConsentTypes];
} else {
// Если нет ни userId, ни walletAddress - все согласия отсутствуют
return {
needsConsent: true,
missingConsents: requiredConsentTypes,
grantedConsents: []
};
}
const consentResult = await db.getQuery()(consentCheckQuery, queryParams);
const grantedConsentTypes = consentResult.rows.map(r => r.consent_type);
const missingConsents = requiredConsentTypes.filter(type => !grantedConsentTypes.includes(type));
const needsConsent = missingConsents.length > 0;
return {
needsConsent,
missingConsents,
grantedConsents: grantedConsentTypes
};
} catch (error) {
logger.error('[ConsentService] Ошибка проверки согласий:', error);
// В случае ошибки считаем, что согласия нужны для безопасности
return {
needsConsent: true,
missingConsents: Object.values(DOCUMENT_CONSENT_MAP),
grantedConsents: []
};
}
}
/**
* Получить список документов для подписания
* @param {Array<string>} missingConsents - Типы недостающих согласий
* @returns {Promise<Array>} - Массив документов с информацией
*/
async function getConsentDocuments(missingConsents = []) {
try {
// Определяем, какие документы нужно подписать (по недостающим типам согласий)
const documentsToShow = Object.entries(DOCUMENT_CONSENT_MAP)
.filter(([title, consentType]) => missingConsents.includes(consentType))
.map(([title]) => title);
if (documentsToShow.length === 0) {
return [];
}
// Получаем список документов для подписания
const { rows: documents } = await db.getQuery()(`
SELECT id, title, summary
FROM admin_pages_simple
WHERE status = 'published'
AND visibility = 'public'
AND title = ANY($1)
ORDER BY created_at DESC
`, [documentsToShow]);
return documents.map(doc => ({
id: doc.id,
title: doc.title,
summary: doc.summary,
consentType: DOCUMENT_CONSENT_MAP[doc.title],
url: `/content/published/${doc.id}`
}));
} catch (error) {
logger.error('[ConsentService] Ошибка получения документов:', error);
return [];
}
}
/**
* Сформировать системное сообщение о необходимости согласия
* @param {Object} params - Параметры
* @param {string} params.channel - Канал (web/telegram/email)
* @param {Array} params.missingConsents - Типы недостающих согласий
* @param {Array} params.consentDocuments - Документы для подписания
* @param {string} params.baseUrl - Базовый URL сайта (для формирования ссылок)
* @returns {Object} - Системное сообщение, отформатированное для канала
*/
function formatConsentMessage({ channel = 'web', missingConsents = [], consentDocuments = [], baseUrl = 'http://localhost:9000' }) {
const baseMessage = 'Для полноценного использования сервиса необходимо предоставить согласие на обработку персональных данных.';
const autoConsentMessage = '\n\n⚠ Внимание: При ответе на это сообщение вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.';
switch (channel) {
case 'web':
// Для веб-чата возвращаем структурированные данные
return {
content: baseMessage + autoConsentMessage,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true, // Флаг для автоматического подписания при ответе
format: 'structured'
};
case 'telegram':
// Для Telegram форматируем как текст с ссылками
let telegramMessage = `${baseMessage}\n\nДля продолжения работы ознакомьтесь со следующими документами:\n\n`;
consentDocuments.forEach((doc, index) => {
telegramMessage += `${index + 1}. ${doc.title}\n`;
if (doc.summary) {
telegramMessage += ` ${doc.summary}\n`;
}
telegramMessage += ` ${baseUrl}${doc.url}\n\n`;
});
telegramMessage += `⚠️ Внимание: При ответе на это сообщение вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.`;
return {
content: telegramMessage,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true,
format: 'text'
};
case 'email':
// Для email форматируем как HTML
let emailHtml = `<p>${baseMessage}</p>`;
emailHtml += '<p>Для продолжения работы ознакомьтесь со следующими документами:</p>';
emailHtml += '<ul>';
consentDocuments.forEach((doc) => {
emailHtml += `<li><strong>${doc.title}</strong><br>`;
if (doc.summary) {
emailHtml += `${doc.summary}<br>`;
}
emailHtml += `<a href="${baseUrl}${doc.url}">Открыть документ</a></li>`;
});
emailHtml += '</ul>';
emailHtml += `<p><strong>⚠️ Внимание:</strong> При ответе на это письмо вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.</p>`;
emailHtml += `<p><a href="${baseUrl}/consent">Также вы можете подписать документы на сайте</a></p>`;
const textContent = baseMessage + '\n\n' +
consentDocuments.map(doc => `${doc.title}: ${baseUrl}${doc.url}`).join('\n') +
'\n\n⚠ Внимание: При ответе на это письмо вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.';
return {
content: emailHtml,
textContent: textContent,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true,
format: 'html'
};
default:
return {
content: baseMessage + autoConsentMessage,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true,
format: 'text'
};
}
}
/**
* Получить полную информацию о согласиях и сформировать системное сообщение
* @param {Object} params - Параметры
* @param {number|null} params.userId - ID пользователя
* @param {string|null} params.walletAddress - Адрес кошелька или guest_ID
* @param {string} params.channel - Канал (web/telegram/email)
* @param {string} params.baseUrl - Базовый URL сайта
* @returns {Promise<Object|null>} - Системное сообщение или null, если согласия есть
*/
async function getConsentSystemMessage({ userId = null, walletAddress = null, channel = 'web', baseUrl = 'http://localhost:9000' }) {
try {
// Проверяем согласия
const consentCheck = await checkConsents({ userId, walletAddress });
if (!consentCheck.needsConsent) {
return null; // Все согласия есть
}
// Получаем документы для подписания
const consentDocuments = await getConsentDocuments(consentCheck.missingConsents);
// Формируем системное сообщение для канала
return formatConsentMessage({
channel,
missingConsents: consentCheck.missingConsents,
consentDocuments,
baseUrl
});
} catch (error) {
logger.error('[ConsentService] Ошибка формирования системного сообщения:', error);
return null;
}
}
module.exports = {
checkConsents,
getConsentDocuments,
formatConsentMessage,
getConsentSystemMessage,
DOCUMENT_CONSENT_MAP
};

View File

@@ -297,7 +297,22 @@ class EmailBot {
const messageData = await this.extractMessageData(parsed, messageId, uid);
if (messageData && this.messageProcessor) {
await this.messageProcessor(messageData);
// Обрабатываем сообщение через унифицированный процессор
// Системное сообщение о согласиях будет добавлено к ответу ИИ внутри процессора
const result = await this.messageProcessor(messageData);
// Если есть ответ ИИ с информацией о согласиях, отправляем email
if (result && result.success && result.aiResponse) {
const fromEmail = parsed.from?.value?.[0]?.address;
if (fromEmail) {
// Ответ ИИ уже содержит системное сообщение о согласиях (если нужно)
await this.sendEmail(
fromEmail,
'Ответ на ваше сообщение',
result.aiResponse.response
);
}
}
}
processedCount++;
@@ -466,6 +481,40 @@ class EmailBot {
}
}
/**
* Отправка email с HTML содержимым
* @param {string} to - Email получателя
* @param {string} subject - Тема письма
* @param {string} text - Текстовая версия
* @param {string} html - HTML версия
*/
async sendEmailWithHtml(to, subject, text, html) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(to)) {
throw new Error(`Неверный формат email адреса: ${to}`);
}
try {
const mailOptions = {
from: this.settings.from_email,
to,
subject,
text,
html
};
await this.transporter.sendMail(mailOptions);
this.transporter.close();
logger.info(`[EmailBot] Email с HTML отправлен успешно: ${to}`);
return true;
} catch (error) {
logger.error('[EmailBot] Ошибка отправки email с HTML:', error);
throw error;
}
}
/**
* Отправка кода верификации
* @param {string} email - Email получателя

View File

@@ -300,26 +300,48 @@ class EncryptedDataService {
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
// Собираем параметры в правильном порядке: сначала для encrypted, потом для unencrypted
const paramsArray = [];
if (hasEncryptedFields) paramsArray.push(this.encryptionKey);
// Собираем параметры в правильном порядке по номерам из плейсхолдеров
const paramMap = new Map(); // номер параметра -> значение
// Добавляем параметры для encrypted колонок
for (const key of Object.keys(encryptedData)) {
const originalKey = key.replace('_encrypted', '');
if (filteredData[originalKey] !== undefined) {
paramsArray.push(filteredData[originalKey]);
} else if (filteredData[originalKey + '_unencrypted'] !== undefined) {
paramsArray.push(filteredData[originalKey + '_unencrypted']);
if (hasEncryptedFields) {
paramMap.set(1, this.encryptionKey); // $1 - ключ шифрования
}
// Проходим по колонкам в порядке allData и добавляем соответствующие значения
for (const key of Object.keys(allData)) {
const placeholder = allData[key].toString();
// Извлекаем все номера параметров из плейсхолдера (может быть $1 в encrypt_text)
const paramMatches = placeholder.match(/\$(\d+)/g);
if (paramMatches) {
// Для зашифрованных колонок нас интересует второй параметр ($3, $4 и т.д.)
// Для незашифрованных - первый параметр ($2, $3 и т.д.)
if (encryptedData[key]) {
// Это зашифрованная колонка - берем второй параметр (первый это $1 - ключ шифрования)
const originalKey = key.replace('_encrypted', '');
if (filteredData[originalKey] !== undefined && paramMatches.length > 0) {
// Последний параметр это значение для шифрования
const valueParam = paramMatches[paramMatches.length - 1];
const paramNum = parseInt(valueParam.substring(1));
paramMap.set(paramNum, filteredData[originalKey]);
}
} else if (unencryptedData[key]) {
// Это незашифрованная колонка - берем параметр из плейсхолдера
const valueParam = paramMatches[0];
const paramNum = parseInt(valueParam.substring(1));
paramMap.set(paramNum, filteredData[key]);
}
}
}
// Добавляем параметры для unencrypted колонок
for (const key of Object.keys(unencryptedData)) {
paramsArray.push(filteredData[key + '_unencrypted'] || filteredData[key]);
// Создаем массив параметров в правильном порядке (от $1 до максимального номера)
const maxParamNum = Math.max(...Array.from(paramMap.keys()));
const params = [];
for (let i = 1; i <= maxParamNum; i++) {
if (!paramMap.has(i)) {
throw new Error(`Отсутствует параметр $${i} для запроса`);
}
params.push(paramMap.get(i));
}
const params = paramsArray;
console.log(`🔍 Выполняем INSERT запрос:`, query);
console.log(`🔍 Параметры:`, params);

View File

@@ -114,6 +114,46 @@ class SessionService {
const identifier = `web:${guestId}`; // Старые гости всегда из web
await universalGuestService.migrateToUser(identifier, userId);
}
// После миграции сообщений переносим согласия по всем гостевых идентификаторам
// Это нужно на случай, если согласия были связаны с гостевой сессией
try {
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
// Получаем wallet адрес пользователя для обновления согласий
const identityService = require('./identity-service');
const walletIdentity = await identityService.findIdentity(userId, 'wallet');
const normalizedWalletAddress = walletIdentity?.provider_id || null;
for (const guestId of guestIdsToProcess) {
// Ищем согласия по гостевому идентификатору в формате "guest_${guestId}"
const guestWalletAddress = `guest_${guestId}`;
const { rows: guestConsents } = await db.getQuery()(`
SELECT id FROM consent_logs
WHERE wallet_address = $1
AND status = 'granted'
AND (user_id IS NULL OR user_id = $2)
`, [guestWalletAddress, userId]);
if (guestConsents.length > 0) {
// Переносим согласия на пользователя и обновляем wallet_address
await db.getQuery()(`
UPDATE consent_logs
SET user_id = $1,
wallet_address = COALESCE($2, wallet_address),
updated_at = NOW()
WHERE id = ANY($3)
`, [userId, normalizedWalletAddress, guestConsents.map(c => c.id)]);
logger.info(`[SessionService] Перенесено ${guestConsents.length} согласий для guest ${guestId} → user ${userId}`);
}
}
} catch (consentError) {
// Не критично, если не удалось перенести согласия - просто логируем
logger.warn(`[SessionService] Ошибка переноса согласий (не критично):`, consentError);
}
}
return { success: true, processedCount: guestIdsToProcess.size };

View File

@@ -382,9 +382,11 @@ class TelegramBot {
}
// Обрабатываем сообщение через унифицированный процессор
// Системное сообщение о согласиях будет добавлено к ответу ИИ внутри процессора
const result = await messageProcessor(messageData);
// Отправляем ответ пользователю
// Системное сообщение о согласиях уже включено в ответ ИИ (если нужно)
if (result.success && result.aiResponse) {
await ctx.reply(result.aiResponse.response);
} else if (result.success) {

View File

@@ -183,6 +183,80 @@ async function processMessage(messageData) {
const conversationId = conversation.id;
// Получаем ключ шифрования (будет использоваться далее)
const encryptionKey = encryptionUtils.getEncryptionKey();
// 5.5. Проверяем, нужно ли автоматически подписать согласие при ответе
// Ищем последнее сообщение от ассистента или системное сообщение с согласием
const consentService = require('./consentService');
const { rows: lastMessages } = await db.getQuery()(
`SELECT
decrypt_text(role_encrypted, $2) as role,
decrypt_text(content_encrypted, $2) as content,
message_type
FROM messages
WHERE conversation_id = $1
AND user_id = $3
AND (
decrypt_text(role_encrypted, $2) = 'assistant'
OR message_type = 'system_consent'
)
ORDER BY created_at DESC
LIMIT 1`,
[conversationId, encryptionKey, userId]
);
// Если последнее сообщение было от ассистента, проверяем наличие системного сообщения о согласиях
if (lastMessages.length > 0) {
// Проверяем согласия пользователя
const walletIdentity = await identityService.findIdentity(userId, 'wallet');
const consentCheck = await consentService.checkConsents({
userId,
walletAddress: walletIdentity?.provider_id || null
});
// Если согласия нужны, но пользователь отвечает на сообщение, автоматически подписываем
if (consentCheck.needsConsent) {
logger.info(`[UnifiedMessageProcessor] Автоматическое подписание согласий при ответе пользователя ${userId}`);
// Получаем документы для подписания
const consentDocuments = await consentService.getConsentDocuments(consentCheck.missingConsents);
const documentIds = consentDocuments.map(doc => doc.id);
const consentTypes = consentDocuments.map(doc => doc.consentType).filter(type => type);
// Автоматически подписываем согласие
if (documentIds.length > 0 && consentTypes.length > 0) {
const consentRoutes = require('../routes/consent');
// Вызываем логику подписания напрямую через сервис или API
try {
await db.getQuery()(
`INSERT INTO consent_logs (user_id, wallet_address, document_id, document_title, consent_type, status, signed_at, channel, ip_address, created_at, updated_at)
SELECT $1, $2, unnest($3::int[]), unnest($4::text[]), unnest($5::text[]), 'granted', NOW(), 'web', NULL, NOW(), NOW()
ON CONFLICT (user_id, consent_type, document_id)
DO UPDATE SET
status = 'granted',
signed_at = NOW(),
revoked_at = NULL,
updated_at = NOW()
WHERE consent_logs.user_id = $1 AND consent_logs.consent_type = EXCLUDED.consent_type`,
[
userId,
walletIdentity?.provider_id || null,
documentIds,
consentDocuments.map(doc => doc.title),
consentTypes
]
);
logger.info(`[UnifiedMessageProcessor] Согласия автоматически подписаны для пользователя ${userId}`);
} catch (consentError) {
logger.error(`[UnifiedMessageProcessor] Ошибка автоматического подписания согласий:`, consentError);
// Не блокируем обработку сообщения при ошибке подписания
}
}
}
}
// 6. Обработка вложений
let attachment_filename = null;
let attachment_mimetype = null;
@@ -198,7 +272,7 @@ async function processMessage(messageData) {
}
// 7. Сохраняем входящее сообщение пользователя
const encryptionKey = encryptionUtils.getEncryptionKey();
// encryptionKey уже объявлен выше
const { rows } = await db.getQuery()(
`INSERT INTO messages (
@@ -293,7 +367,31 @@ async function processMessage(messageData) {
});
if (aiResponse && aiResponse.success && aiResponse.response) {
// Сохраняем ответ AI
// Проверяем согласия и добавляем системное сообщение к ответу ИИ
const walletIdentity = await identityService.findIdentity(userId, 'wallet');
const consentSystemMessage = await consentService.getConsentSystemMessage({
userId,
walletAddress: walletIdentity?.provider_id || null,
channel: channel === 'web' ? 'web' : channel,
baseUrl: process.env.BASE_URL || 'http://localhost:9000'
});
// Формируем финальный ответ ИИ с системным сообщением, если нужно
let finalAiResponse = aiResponse.response;
if (consentSystemMessage && consentSystemMessage.consentRequired) {
// Добавляем системное сообщение к ответу ИИ
finalAiResponse = `${aiResponse.response}\n\n---\n\n${consentSystemMessage.content}`;
// Сохраняем информацию о согласиях в метаданные ответа
aiResponse.consentInfo = {
consentRequired: true,
missingConsents: consentSystemMessage.missingConsents,
consentDocuments: consentSystemMessage.consentDocuments,
autoConsentOnReply: consentSystemMessage.autoConsentOnReply
};
}
// Сохраняем ответ AI с добавленным системным сообщением
const { rows: aiMessageRows } = await db.getQuery()(
`INSERT INTO messages (
conversation_id,
@@ -322,7 +420,7 @@ async function processMessage(messageData) {
conversationId,
userId, // sender_id
'assistant',
aiResponse.response,
finalAiResponse,
channel,
'assistant',
'outgoing',
@@ -353,17 +451,27 @@ async function processMessage(messageData) {
}
// 11. Возвращаем результат
return {
const result = {
success: true,
userMessageId,
conversationId,
aiResponse: aiResponse && aiResponse.success ? {
response: aiResponse.response,
response: finalAiResponse || aiResponse.response,
ragData: aiResponse.ragData
} : null,
noAiResponse: !shouldGenerateAi
};
// Если есть информация о согласиях, добавляем её в результат
if (aiResponse && aiResponse.success && aiResponse.consentInfo) {
result.consentRequired = aiResponse.consentInfo.consentRequired;
result.missingConsents = aiResponse.consentInfo.missingConsents;
result.consentDocuments = aiResponse.consentInfo.consentDocuments;
result.autoConsentOnReply = aiResponse.consentInfo.autoConsentOnReply;
}
return result;
} catch (error) {
logger.error('[UnifiedMessageProcessor] Ошибка обработки сообщения:', error);
throw error;