summy/bot.js
2025-06-30 21:42:55 +03:00

584 lines
23 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');
const logger = require('./logger.js');
const callAI = require("./requestAI");
const {searchInPrompts, saveInPrompts} = require("./promptResponser");
const INSTRUCTIONS = require("./promptGen");
const {sendHelp} = require("./infoSender");
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
}
}
getUniqueChatIds() {
const ids = new Set();
for (const msg of this.history) {
ids.add(msg.chat_id);
}
return Array.from(ids);
}
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() {
const isGroupChat = (ctx) => {
return ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
};
this.bot.start(async (ctx) => {
if (isGroupChat(ctx)) {
return; // Игнорируем в группах
}
await sendHelp(ctx);
});
this.bot.command('summy', async (ctx) => {
if (!(await this.isAdmin(ctx))) {
await ctx.deleteMessage()
return
}
if (!isGroupChat(ctx)) {
await ctx.reply('❗ Эта команда работает только в группах');
return;
}
const message = ctx.message.text || '';
await ctx.deleteMessage();
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('getchats', async (ctx) => {
if (ctx.message.chat.id!==Number(process.env.ADMIN_CHAT_ID)) {
logger.warn(JSON.stringify(ctx.message))
logger.warn('Попытка вызова списка чатов из стороннего чата. Пользователь ' + JSON.stringify(ctx.message.from))
return
}
const chatIds = this.getUniqueChatIds();
await ctx.reply(JSON.stringify(chatIds))
});
// Команды суммаризации - должны быть ДО обработки обычных сообщений
// 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 (!isGroupChat(ctx)) {
return;
}
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,
chat_title: msg.chat.title || null,
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 = `📊 <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) {
logger.error('❌ Укажите токен бота в переменной окружения BOT_TOKEN');
process.exit(1);
}
const bot = new TelegramHistoryBot(BOT_TOKEN);
module.exports = TelegramHistoryBot;