ваше сообщение коммита

This commit is contained in:
2025-11-14 12:34:24 +03:00
parent f37e9e1428
commit bbf1c6aa5a
7 changed files with 726 additions and 110 deletions

View File

@@ -319,6 +319,101 @@ class UniversalGuestService {
}
}
/**
* Извлечь имя гостя из текста сообщения через ИИ и сохранить в metadata
* @param {string} identifier - Идентификатор гостя
* @param {string} content - Текст сообщения
* @param {string} channel - Канал
* @returns {Promise<void>}
*/
async extractAndSaveGuestName(identifier, content, channel) {
try {
if (!content || !content.trim()) {
return; // Нет текста для анализа
}
const encryptionKey = encryptionUtils.getEncryptionKey();
// Находим первое сообщение гостя
const firstMessageResult = await db.getQuery()(
`WITH decrypted_guest AS (
SELECT
id,
decrypt_text(identifier_encrypted, $2) as guest_identifier,
channel,
metadata
FROM unified_guest_messages
WHERE user_id IS NULL
)
SELECT
MIN(id) as first_message_id,
MIN(metadata) as metadata
FROM decrypted_guest
WHERE guest_identifier = $1 AND channel = $3
GROUP BY guest_identifier, channel`,
[identifier, encryptionKey, channel]
);
if (firstMessageResult.rows.length === 0) {
return; // Гость не найден
}
const firstMessage = firstMessageResult.rows[0];
let metadata = firstMessage.metadata || {};
// Если metadata - строка, парсим её
if (typeof metadata === 'string') {
try {
metadata = JSON.parse(metadata);
} catch (e) {
metadata = {};
}
}
// Если уже есть кастомное имя, не извлекаем заново
if (metadata.custom_name) {
logger.info(`[UniversalGuestService] У гостя ${identifier} уже есть кастомное имя: ${metadata.custom_name}`);
return;
}
// Используем существующий сервис для извлечения имени
const profileAnalysisService = require('./profileAnalysisService');
const nameResult = await profileAnalysisService.extractName(content);
// Проверяем результат извлечения имени
if (!nameResult || !nameResult.name || !nameResult.should_update_name) {
logger.info(`[UniversalGuestService] Имя не найдено в сообщении гостя ${identifier} (confidence: ${nameResult?.confidence || 0})`);
return;
}
const extractedName = nameResult.name;
// Разбиваем имя на части
const nameParts = extractedName.split(' ');
const firstName = nameParts[0] || '';
const lastName = nameParts.slice(1).join(' ') || '';
// Сохраняем имя в metadata
metadata.custom_name = extractedName;
metadata.custom_first_name = firstName;
metadata.custom_last_name = lastName;
// Обновляем metadata первого сообщения гостя
await db.getQuery()(
`UPDATE unified_guest_messages
SET metadata = $1
WHERE id = $2`,
[JSON.stringify(metadata), firstMessage.first_message_id]
);
logger.info(`[UniversalGuestService] Имя гостя ${identifier} извлечено и сохранено: ${extractedName}`);
} catch (error) {
logger.error('[UniversalGuestService] Ошибка извлечения имени гостя:', error);
throw error;
}
}
/**
* Обработать сообщение гостя (сохранить + получить AI ответ)
* @param {Object} messageData
@@ -398,6 +493,12 @@ class UniversalGuestService {
const saveResult = await this.saveMessage(messageData);
const processedContent = saveResult.processedContent;
// 1.5. Извлекаем имя из текста сообщения через ИИ (если это первое сообщение гостя)
await this.extractAndSaveGuestName(identifier, content, channel).catch(error => {
// Не критично, если не удалось извлечь имя - просто логируем
logger.warn(`[UniversalGuestService] Ошибка извлечения имени гостя:`, error);
});
// 2. Загружаем историю для контекста (заново, так как могли добавиться сообщения)
const conversationHistory = await this.getHistory(identifier);

View File

@@ -70,16 +70,24 @@ async function saveBotSettings(botType, settings) {
throw new Error(`Unknown bot type: ${botType}`);
}
// Простое сохранение - детали зависят от структуры таблицы
const { rows } = await db.getQuery()(
`INSERT INTO ${tableName} (settings, updated_at)
VALUES ($1, NOW())
ON CONFLICT (id) DO UPDATE SET settings = $1, updated_at = NOW()
RETURNING *`,
[JSON.stringify(settings)]
);
const dataToSave = {
...settings,
updated_at: new Date()
};
return rows[0];
// Проверяем, существуют ли записи в таблице
const existing = await encryptedDb.getData(tableName, {}, 1);
if (existing.length > 0) {
// Обновляем первую запись (ожидаем, что таблица хранит единственную конфигурацию)
return await encryptedDb.saveData(tableName, dataToSave, { id: existing[0].id });
}
// Если записей нет, создаем новую
return await encryptedDb.saveData(tableName, {
...dataToSave,
created_at: new Date()
});
} catch (error) {
logger.error(`[BotsSettings] Ошибка сохранения настроек ${botType}:`, error);

View File

@@ -33,6 +33,7 @@ class EmailBot {
this.status = 'inactive';
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 3;
this.periodicCheckInterval = null;
}
/**
@@ -154,7 +155,9 @@ class EmailBot {
authTimeout: 60000,
greetingTimeout: 30000,
socketTimeout: 60000,
debug: false
debug: (info) => {
logger.debug(`[EmailBot IMAP] ${info}`);
}
});
// Настраиваем обработчики событий
@@ -177,6 +180,8 @@ class EmailBot {
logger.info('[EmailBot] IMAP соединение установлено');
this.reconnectAttempts = 0;
this.checkEmails();
// Запускаем периодическую проверку новых писем каждые 5 минут
this.startPeriodicCheck();
});
this.imap.once('end', () => {
@@ -200,6 +205,9 @@ class EmailBot {
* Очистка IMAP соединения
*/
cleanupImap() {
// Останавливаем периодическую проверку
this.stopPeriodicCheck();
if (this.imap) {
try {
this.imap.removeAllListeners('error');
@@ -244,22 +252,66 @@ class EmailBot {
setTimeout(() => this.initializeImap(), reconnectDelay);
}
/**
* Запуск периодической проверки новых писем
*/
startPeriodicCheck() {
// Останавливаем предыдущий интервал, если он есть
this.stopPeriodicCheck();
// Проверяем новые письма каждые 5 минут
this.periodicCheckInterval = setInterval(() => {
if (this.imap && this.imap.state === 'authenticated') {
logger.info('[EmailBot] Периодическая проверка новых писем...');
this.checkEmails();
} else {
logger.warn('[EmailBot] IMAP соединение не активно, пропускаем периодическую проверку');
}
}, 5 * 60 * 1000); // 5 минут
logger.info('[EmailBot] Периодическая проверка новых писем запущена (каждые 5 минут)');
}
/**
* Остановка периодической проверки
*/
stopPeriodicCheck() {
if (this.periodicCheckInterval) {
clearInterval(this.periodicCheckInterval);
this.periodicCheckInterval = null;
logger.info('[EmailBot] Периодическая проверка остановлена');
}
}
/**
* Проверка входящих писем
*/
checkEmails() {
try {
logger.info('[EmailBot] Проверка входящих писем...');
this.imap.openBox('INBOX', false, (err, box) => {
if (err) {
logger.error('[EmailBot] Ошибка открытия INBOX:', err);
return;
}
this.imap.search(['ALL'], (err, results) => {
if (err || !results || results.length === 0) {
this.imap.end();
logger.info(`[EmailBot] INBOX открыт. Всего сообщений: ${box.messages.total}`);
// Ищем только непрочитанные сообщения (UNSEEN)
this.imap.search(['UNSEEN'], (err, results) => {
if (err) {
logger.error('[EmailBot] Ошибка поиска писем:', err);
// Не закрываем соединение при ошибке поиска, оставляем его открытым для keepalive
return;
}
if (!results || results.length === 0) {
logger.info('[EmailBot] Новых непрочитанных писем нет');
// Не закрываем соединение, оставляем его открытым для keepalive
return;
}
logger.info(`[EmailBot] Найдено ${results.length} непрочитанных писем`);
const f = this.imap.fetch(results, {
bodies: '',
@@ -284,9 +336,11 @@ class EmailBot {
msg.on('body', (stream, info) => {
simpleParser(stream, async (err, parsed) => {
if (err) {
logger.error(`[EmailBot] Ошибка парсинга письма ${seqno}:`, err);
processedCount++;
if (processedCount >= totalMessages) {
this.imap.end();
logger.info('[EmailBot] Обработка всех писем завершена');
// Не закрываем соединение, оставляем его открытым для keepalive
}
return;
}
@@ -295,29 +349,44 @@ class EmailBot {
messageId = parsed.messageId;
}
const fromEmail = parsed.from?.value?.[0]?.address;
logger.info(`[EmailBot] Обработка письма ${seqno} от ${fromEmail || 'неизвестного отправителя'}`);
const messageData = await this.extractMessageData(parsed, messageId, uid);
if (messageData && this.messageProcessor) {
// Обрабатываем сообщение через унифицированный процессор
// Системное сообщение о согласиях будет добавлено к ответу ИИ внутри процессора
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
);
try {
// Обрабатываем сообщение через унифицированный процессор
// Системное сообщение о согласиях будет добавлено к ответу ИИ внутри процессора
const result = await this.messageProcessor(messageData);
logger.info(`[EmailBot] Письмо ${seqno} обработано успешно`);
// Если есть ответ ИИ с информацией о согласиях, отправляем email
if (result && result.success && result.aiResponse) {
if (fromEmail) {
logger.info(`[EmailBot] Отправка ответа ИИ на ${fromEmail}`);
// Ответ ИИ уже содержит системное сообщение о согласиях (если нужно)
await this.sendEmail(
fromEmail,
'Ответ на ваше сообщение',
result.aiResponse.response
);
}
}
} catch (processError) {
logger.error(`[EmailBot] Ошибка обработки письма ${seqno}:`, processError);
}
} else {
if (!messageData) {
logger.warn(`[EmailBot] Письмо ${seqno} отфильтровано (системное или некорректное)`);
} else if (!this.messageProcessor) {
logger.warn('[EmailBot] messageProcessor не установлен, письмо не обработано');
}
}
processedCount++;
if (processedCount >= totalMessages) {
this.imap.end();
logger.info('[EmailBot] Обработка всех писем завершена');
// Не закрываем соединение, оставляем его открытым для keepalive
}
});
});
@@ -325,7 +394,7 @@ class EmailBot {
f.once('error', (err) => {
logger.error('[EmailBot] Ошибка получения писем:', err);
this.imap.end();
// Не закрываем соединение при ошибке fetch, оставляем его открытым для keepalive
});
});
});