const { Telegraf } = require('telegraf'); const fs = require('fs').promises; const path = require('path'); const { OpenRouterClient } = require('openrouter-kit'); const { getPrompt } = require('./prompts.js'); const logger = require('./logger.js'); const callAI = require("./requestAI"); const {searchInPrompts, saveInPrompts} = require("./promptResponser"); const INSTRUCTIONS = require("./promptGen"); require('dotenv').config(); const char = {name:'marina'} class TelegramHistoryBot { constructor(token) { this.bot = new Telegraf(token); this.historyFile = path.join(__dirname, 'chat_history.json'); this.history = []; // Инициализация OpenRouter с проверкой ключа if (!process.env.OPENROUTER_API_KEY) { logger.warn('⚠️ OPENROUTER_API_KEY не найден, ИИ-суммаризация будет недоступна'); this.openRouter = null; } else { this.openRouter = new OpenRouterClient({ apiKey: process.env.OPENROUTER_API_KEY, appName: 'telegram-history-bot', appVersion: '1.0.0' }); } this.init(); } async isAdmin(ctx, userId = ctx.from.id) { try { const member = await ctx.getChatMember(userId) return ['creator', 'administrator'].includes(member.status) } catch (error) { logger.error('Ошибка проверки статуса пользователя:', error) return false } } async init() { try { await this.loadHistory(); this.setupHandlers(); logger.info('✅ Bot запущен и готов к работе'); } catch (error) { logger.error('❌ Ошибка инициализации бота:', error); process.exit(1); } } async loadHistory() { try { const data = await fs.readFile(this.historyFile, 'utf8'); this.history = JSON.parse(data); logger.info(`📚 Загружено ${this.history.length} сообщений из истории`); } catch (error) { logger.warn('📝 История не найдена, создаем новую'); this.history = []; await this.saveHistory(); } } async saveHistory() { try { await fs.writeFile(this.historyFile, JSON.stringify(this.history, null, 2)); } catch (error) { logger.error('❌ Ошибка сохранения истории:', error); } } setupHandlers() { this.bot.command('summy', async (ctx) => { if (!(await this.isAdmin(ctx))) { await ctx.deleteMessage() return } const message = ctx.message.text || ''; const args = message.replace(/^\/summy(@\w+)?\s*/, ''); // удаляет /summy и возможный @botname const trimmed = args.trim(); // удаляет лишние пробелы по краям, если нужно logger.info('📊 Получена команда /summy с запросом: ' + trimmed); let request = await callAI('', trimmed, 'request'); if (typeof request === 'string') { try { request = JSON.parse(request); } catch { request = {persona: "Карл Маркс", messages: 22, hours: 0}; } } let prompt = await searchInPrompts(request["persona"]) if (!prompt) { logger.info('⚠️ Не найден подходящий prompt для запроса, генерируем...') prompt=await callAI(INSTRUCTIONS, request["persona"], 'prompt') await saveInPrompts(request["persona"], prompt) } else { logger.info('✅ Найден подходящий prompt для запроса') } const options = {persona:request["persona"], promptToUse:prompt} if (request["messages"]>0) { await this.handleSummaryCommand(ctx, 'last', request["messages"], options); } else { await this.handleSummaryCommand(ctx, 'hours', request["hours"], options); } }); // Команды суммаризации - должны быть ДО обработки обычных сообщений // this.bot.command('summary_day', async (ctx) => { // // if (!this.isAdmin(ctx)) { // // await ctx.deleteMessage // // return // // } // logger.info('📊 Получена команда summary_day'); // const args = ctx.message.text.split(' '); // if (args.length > 1) { // char.name=args[1] // } // await ctx.deleteMessage() // await this.handleSummaryCommand(ctx, 'day'); // }); // // this.bot.command('summary_hours', async (ctx) => { // logger.info('📊 Получена команда summary_hours'); // const args = ctx.message.text.split(' '); // if (args.length < 2 || isNaN(parseInt(args[1]))) { // await ctx.reply('❗ Укажите количество часов: /summary_hours 6'); // return; // } // const hours = parseInt(args[1]); // await ctx.deleteMessage() // await this.handleSummaryCommand(ctx, 'hours', hours); // }); // // this.bot.command('summary_last', async (ctx) => { // logger.info('📊 Получена команда summary_last'); // const args = ctx.message.text.split(' '); // if (args.length < 2 || isNaN(parseInt(args[1]))) { // await ctx.reply('❗ Укажите количество сообщений: /summary_last 50'); // return; // } // const count = parseInt(args[1]); // await ctx.deleteMessage() // await this.handleSummaryCommand(ctx, 'last', count); // }); // Команды помощи //this.bot.command('summary_help', async (ctx) => await this.sendHelp(ctx)); //this.bot.start(async (ctx) => await this.sendHelp(ctx)); // Общий обработчик сообщений (исключая команды) this.bot.on('message', async (ctx) => { // Пропускаем все команды if (ctx.message.text && ctx.message.text.startsWith('/')) { return; } await this.handleMessage(ctx); }); // Обработка ошибок this.bot.catch(async (err, ctx) => { logger.error('❌ Ошибка бота:', err); try { await ctx.reply('❌ Произошла ошибка при обработке команды'); } catch (replyError) { logger.error('❌ Не удалось отправить сообщение об ошибке:', replyError); } }); // Запуск бота this.bot.launch(); logger.info('🚀 Бот запущен'); // Graceful stop process.once('SIGINT', () => { logger.warn('🛑 Получен сигнал SIGINT, останавливаем бота...'); this.bot.stop('SIGINT'); }); process.once('SIGTERM', () => { logger.warn('🛑 Получен сигнал SIGTERM, останавливаем бота...'); this.bot.stop('SIGTERM'); }); } async handleMessage(ctx) { try { const msg = ctx.message; const messageData = { id: this.generateUniqueId(), telegram_message_id: msg.message_id, chat_id: msg.chat.id, user_id: msg.from.id, username: msg.from.username || null, first_name: msg.from.first_name || null, last_name: msg.from.last_name || null, text: msg.text || null, timestamp: new Date().toISOString(), date: msg.date * 1000, reply_to_message_id: msg.reply_to_message ? msg.reply_to_message.message_id : null, reply_to_user_id: msg.reply_to_message ? msg.reply_to_message.from.id : null, message_type: this.getMessageType(msg), media_info: this.extractMediaInfo(msg) }; this.history.push(messageData); await this.saveHistory(); const userName = messageData.first_name || messageData.username || 'Unknown'; const content = messageData.text || `[${messageData.message_type}]`; logger.info(`💾 Сохранено сообщение от ${userName} в ${msg.chat.id} : ${content.substring(0, 50)}${content.length > 50 ? '...' : ''}`); } catch (error) { logger.error('❌ Ошибка при обработке сообщения:', error); } } generateUniqueId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } getMessageType(msg) { if (msg.text) return 'text'; if (msg.photo) return 'photo'; if (msg.video) return 'video'; if (msg.audio) return 'audio'; if (msg.voice) return 'voice'; if (msg.document) return 'document'; if (msg.sticker) return 'sticker'; if (msg.animation) return 'animation'; if (msg.video_note) return 'video_note'; if (msg.location) return 'location'; if (msg.contact) return 'contact'; return 'other'; } extractMediaInfo(msg) { const info = {}; if ((msg.photo || msg.video) && msg.caption) { info.caption = msg.caption; } if (msg.document) { info.file_name = msg.document.file_name; if (msg.caption) info.caption = msg.caption; } if (msg.sticker) { info.emoji = msg.sticker.emoji; } if (msg.location) { info.latitude = msg.location.latitude; info.longitude = msg.location.longitude; } if (msg.contact) { info.phone_number = msg.contact.phone_number; info.first_name = msg.contact.first_name; } return Object.keys(info).length > 0 ? info : null; } async handleSummaryCommand(ctx, type, value, options = {}) { logger.info(`📊 Обработка команды суммаризации: ${type}${value ? ` (${value})` : ''}`); const chatId = ctx.chat.id; let messages = []; try { // Показываем индикатор "печатает" await ctx.replyWithChatAction('typing'); // Получаем сообщения switch (type) { case 'day': messages = this.getMessagesForDay(chatId); break; case 'hours': if (!value || value < 1 || value > 168) { await ctx.reply('❗ Количество часов должно быть от 1 до 168'); return; } messages = this.getMessagesForHours(chatId, value); break; case 'last': if (!value || value < 1 || value > 1000) { await ctx.reply('❗ Количество сообщений должно быть от 1 до 1000'); return; } messages = this.getLastMessages(chatId, value); break; } logger.info(`📈 Найдено ${messages.length} сообщений для суммаризации`); if (messages.length === 0) { await ctx.reply('❗ Сообщения для суммаризации не найдены'); return; } if (messages.length < 3) { await ctx.reply('❗ Слишком мало сообщений для создания качественной суммаризации (минимум 3)'); return; } // Генерируем суммаризацию const summary = await this.generateSummary(messages, type, value, options); // Отправляем результат await ctx.reply(summary, { parse_mode: 'HTML' }); logger.info('✅ Суммаризация отправлена'); } catch (error) { logger.error('❌ Ошибка при создании суммаризации:', error); await ctx.reply('❌ Произошла ошибка при создании суммаризации. Попробуйте позже.'); } } getMessagesForDay(chatId) { const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000); return this.history.filter(msg => msg.chat_id === chatId && new Date(msg.timestamp).getTime() >= oneDayAgo ); } getMessagesForHours(chatId, hours) { const timeAgo = Date.now() - (hours * 60 * 60 * 1000); return this.history.filter(msg => msg.chat_id === chatId && new Date(msg.timestamp).getTime() >= timeAgo ); } getLastMessages(chatId, count) { return this.history .filter(msg => msg.chat_id === chatId) .slice(-count); } async generateSummary(messages, type, value, options = {}) { const preparedData = this.prepareDataForAI(messages); if (options) { return await this.callAISummarization('Ты получишь данные чата. ' + options.promptToUse, preparedData); } else { const prompt = this.createSummaryPrompt(type, value); return await this.callAISummarization(prompt, preparedData); } } prepareDataForAI(messages) { const participants = this.getUniqueParticipants(messages); const mostActiveUser = this.getMostActiveUser(messages); const conversationIntensity = this.getConversationIntensity(messages); const messageTypesStats = this.getMessageTypesStats(messages); return { metadata: { total_messages: messages.length, time_range: { start: messages[0]?.timestamp, end: messages[messages.length - 1]?.timestamp }, participants, most_active_user: mostActiveUser, conversation_intensity: conversationIntensity, message_types: messageTypesStats }, raw_messages: messages.map(msg => ({ id: msg.id, user: `${msg.first_name}${msg.last_name ? ' ' + msg.last_name : ''}`, username: msg.username, display_name: msg.username ? `@${msg.username}` : `${msg.first_name}${msg.last_name ? ' ' + msg.last_name : ''}`, text: msg.text, timestamp: msg.timestamp, message_type: msg.message_type, reply_to: msg.reply_to_message_id, media_info: msg.media_info, is_media: msg.message_type !== 'text' })) }; } getUniqueParticipants(messages) { const participants = new Map(); messages.forEach(msg => { if (!participants.has(msg.user_id)) { participants.set(msg.user_id, { user_id: msg.user_id, name: `${msg.first_name}${msg.last_name ? ' ' + msg.last_name : ''}`, username: msg.username, message_count: 0 }); } participants.get(msg.user_id).message_count++; }); return Array.from(participants.values()); } getMostActiveUser(messages) { const participants = this.getUniqueParticipants(messages); return participants.reduce((max, participant) => participant.message_count > max.message_count ? participant : max, participants[0] ); } getConversationIntensity(messages) { if (messages.length < 2) return 'low'; const timeSpan = new Date(messages[messages.length - 1].timestamp) - new Date(messages[0].timestamp); const hoursSpan = timeSpan / (1000 * 60 * 60); if (hoursSpan === 0) return 'high'; const messagesPerHour = messages.length / hoursSpan; if (messagesPerHour > 10) return 'high'; if (messagesPerHour > 3) return 'medium'; return 'low'; } getMessageTypesStats(messages) { const stats = {}; messages.forEach(msg => { stats[msg.message_type] = (stats[msg.message_type] || 0) + 1; }); return stats; } createSummaryPrompt(type, value) { const timeDescription = this.getTimeDescription(type, value); logger.info('Выбранный персонаж: ', char) return `Ты получишь данные чата Telegram ${timeDescription}. ` + getPrompt(char.name) } getTimeDescription(type, value) { switch (type) { case 'day': return 'за последние 24 часа'; case 'hours': return `за последние ${value} часов`; case 'last': return `последние ${value} сообщений`; default: return ''; } } formatMessagesForAI(data) { const { metadata, raw_messages } = data; const participantsList = metadata.participants .map(p => `${p.name}${p.username ? ` (@${p.username})` : ''} - ${p.message_count} сообщений`) .join('\n'); const conversationFlow = raw_messages .map(msg => { const author = msg.display_name; const content = msg.is_media ? `[${msg.message_type}${msg.media_info?.caption ? `: ${msg.media_info.caption}` : ''}]` : msg.text; const replyMarker = msg.reply_to ? ' (ответ)' : ''; const timestamp = new Date(msg.timestamp).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); return `[${timestamp}] ${author}${replyMarker}: ${content}`; }) .join('\n'); return `УЧАСТНИКИ ЧАТА: ${participantsList} ОБЩАЯ ИНФОРМАЦИЯ: - Всего сообщений: ${metadata.total_messages} - Период: ${new Date(metadata.time_range.start).toLocaleString('ru-RU')} - ${new Date(metadata.time_range.end).toLocaleString('ru-RU')} - Самый активный: ${metadata.most_active_user.name} (${metadata.most_active_user.message_count} сообщений) - Интенсивность беседы: ${metadata.conversation_intensity} ХОД РАЗГОВОРА: ${conversationFlow}`; } async callAISummarization(prompt, data) { return await callAI(prompt, this.formatMessagesForAI(data), 'history') } generateFallbackSummary(data) { const { metadata } = data; const mostActive = metadata.most_active_user; const participantsCount = metadata.participants.length; const messageTypes = Object.keys(metadata.message_types); let summary = `📊 Краткая сводка чата\n\n`; summary += `📝 Всего сообщений: ${metadata.total_messages}\n`; summary += `👥 Участников: ${participantsCount}\n`; summary += `🏆 Самый активный: ${mostActive.name} (${mostActive.message_count} сообщений)\n`; summary += `⚡ Интенсивность: ${metadata.conversation_intensity}\n`; if (messageTypes.length > 1) { summary += `📱 Типы сообщений: ${messageTypes.join(', ')}\n`; } summary += `\n⚠️ Подробная ИИ-суммаризация временно недоступна`; return summary; } async sendHelp(ctx) { const helpText = ` 🤖 Бот сохраняет всю историю чата 📊 Команды суммаризации: /summary_day - суммаризация за сутки /summary_hours N - за последние N часов /summary_last N - последние N сообщений 💡 Примеры: /summary_hours 6 - за последние 6 часов /summary_last 50 - последние 50 сообщений ℹ️ Бот сохраняет все сообщения и создает живые истории на основе переписки. 🔧 Поддерживаются все типы сообщений: текст, фото, видео, аудио, документы, стикеры и др. `.trim(); await ctx.reply(helpText, { parse_mode: 'HTML' }); } } // Запуск бота const BOT_TOKEN = process.env.BOT_TOKEN; if (!BOT_TOKEN) { logger.error('❌ Укажите токен бота в переменной окружения BOT_TOKEN'); process.exit(1); } const bot = new TelegramHistoryBot(BOT_TOKEN); module.exports = TelegramHistoryBot;