summy/bot.js

567 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { Telegraf } = require('telegraf');
const fs = require('fs').promises;
const path = require('path');
const { OpenRouterClient } = require('openrouter-kit');
const { getPrompt } = require('./prompts.js');
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) {
console.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();
}
isAdmin (ctx, userId = ctx.from.id) {
try {
const member = ctx.getChatMember(userId)
return ['creator', 'administrator'].includes(member.status)
} catch {
return false
}
}
async init() {
try {
await this.loadHistory();
this.setupHandlers();
console.log('✅ Bot запущен и готов к работе');
} catch (error) {
console.error('❌ Ошибка инициализации бота:', error);
process.exit(1);
}
}
async loadHistory() {
try {
const data = await fs.readFile(this.historyFile, 'utf8');
this.history = JSON.parse(data);
console.log(`📚 Загружено ${this.history.length} сообщений из истории`);
} catch (error) {
console.log('📝 История не найдена, создаем новую');
this.history = [];
await this.saveHistory();
}
}
async saveHistory() {
try {
await fs.writeFile(this.historyFile, JSON.stringify(this.history, null, 2));
} catch (error) {
console.error('❌ Ошибка сохранения истории:', error);
}
}
setupHandlers() {
// Команды суммаризации - должны быть ДО обработки обычных сообщений
this.bot.command('summary_day', async (ctx) => {
// if (!this.isAdmin(ctx)) {
// await ctx.deleteMessage
// return
// }
console.log('📊 Получена команда 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) => {
console.log('📊 Получена команда 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) => {
console.log('📊 Получена команда 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) => {
console.error('❌ Ошибка бота:', err);
try {
await ctx.reply('❌ Произошла ошибка при обработке команды');
} catch (replyError) {
console.error('❌ Не удалось отправить сообщение об ошибке:', replyError);
}
});
// Запуск бота
this.bot.launch();
console.log('🚀 Бот запущен');
// Graceful stop
process.once('SIGINT', () => {
console.log('🛑 Получен сигнал SIGINT, останавливаем бота...');
this.bot.stop('SIGINT');
});
process.once('SIGTERM', () => {
console.log('🛑 Получен сигнал 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}]`;
console.log(`💾 Сохранено сообщение от ${userName}: ${content.substring(0, 50)}${content.length > 50 ? '...' : ''}`);
} catch (error) {
console.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) {
console.log(`📊 Обработка команды суммаризации: ${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;
}
console.log(`📈 Найдено ${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);
// Отправляем результат
await ctx.reply(summary, { parse_mode: 'HTML' });
console.log('✅ Суммаризация отправлена');
} catch (error) {
console.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) {
const preparedData = this.prepareDataForAI(messages);
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);
console.log('Выбранный персонаж: ', 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) {
// Если API ключ недоступен, используем fallback
if (!process.env.OPENROUTER_API_KEY) {
console.log('⚠️ OpenRouter API ключ недоступен, используем базовую суммаризацию');
return this.generateFallbackSummary(data);
}
try {
const formattedMessages = this.formatMessagesForAI(data);
console.log('🤖 Отправляем запрос к OpenRouter...');
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/your-repo', // Замените на ваш репозиторий
'X-Title': 'Telegram History Bot'
},
body: JSON.stringify({
model: 'google/gemini-2.5-flash-preview-05-20',
// model: 'google/gemini-2.0-flash-exp:free',
// model: 'deepseek/deepseek-chat-v3-0324:free',
messages: [
{
role: 'system',
content: prompt
},
{
role: 'user',
content: formattedMessages
}
],
max_tokens: 2000,
temperature: 0.8,
top_p: 0.9
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OpenRouter API error: ${response.status} - ${errorText}`);
}
const completion = await response.json();
const aiSummary = completion.choices?.[0]?.message?.content;
if (!aiSummary) {
throw new Error('Пустой ответ от ИИ');
}
console.log('✅ Получен ответ от OpenRouter');
return `🎬 <b>История чата</b>\n\n${aiSummary}`;
} catch (error) {
console.error('❌ Ошибка при вызове OpenRouter API:', error);
// Детальная обработка различных типов ошибок
if (error.message.includes('401')) {
console.error('❌ Неверный API ключ OpenRouter');
} else if (error.message.includes('429')) {
console.error('❌ Превышен лимит запросов OpenRouter');
} else if (error.message.includes('402')) {
console.error('❌ Недостаточно средств на счету OpenRouter');
} else if (error.code === 'ENOTFOUND') {
console.error('❌ Проблемы с сетевым подключением');
}
return this.generateFallbackSummary(data);
}
}
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 = `📊 <b>Краткая сводка чата</b>\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<i>⚠️ Подробная ИИ-суммаризация временно недоступна</i>`;
return summary;
}
async sendHelp(ctx) {
const helpText = `
🤖 <b>Бот сохраняет всю историю чата</b>
📊 <b>Команды суммаризации:</b>
/summary_day - суммаризация за сутки
/summary_hours N - за последние N часов
/summary_last N - последние N сообщений
💡 <b>Примеры:</b>
/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) {
console.error('❌ Укажите токен бота в переменной окружения BOT_TOKEN');
process.exit(1);
}
const bot = new TelegramHistoryBot(BOT_TOKEN);
module.exports = TelegramHistoryBot;