From 18611835cb3a242713b3678c0cbc2882b380c0fb Mon Sep 17 00:00:00 2001 From: Vufer Date: Sun, 29 Jun 2025 22:54:46 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D1=83=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B=20/summy=20?= =?UTF-8?q?=D1=81=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=81=D0=BE=D0=BD=D0=B0=D0=B6=D0=B0=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=81=D1=83=D0=BC=D0=BC=D0=B0=D1=80=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен парсер запроса из текста команды с разбором имени персонажа и временного периода/количества сообщений - Внедрена кэшированная генерация и поиск промптов по персонажу для более живых и контекстных суммаризаций - Интегрирован внешний вызов API OpenRouter для разбора команд и генерации суммаризаций с учетом стиля персонажа - Обновлен основной класс TelegramHistoryBot для поддержки новой команды и вызова AI через requestAI.js - Добавлены хранилища кэша для команд и промптов с логированием загрузки, сохранения и ошибок - Созданы инструкции для генератора промптов с детальной структурой и правилами для разнообразных персонажей BREAKING CHANGE: Для корректной работы требуется добавить в .env ключи OPENROUTER_API_KEY, OPENROUTER_MODEL и OPENROUTER_CHEAP_MODEL --- bot.js | 124 ++++++++++--------------- commandResponser.js | 75 +++++++++++++++ promptGen.js | 73 +++++++++++++++ promptResponser.js | 105 +++++++++++++++++++++ requestAI.js | 218 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 519 insertions(+), 76 deletions(-) create mode 100644 commandResponser.js create mode 100644 promptGen.js create mode 100644 promptResponser.js create mode 100644 requestAI.js diff --git a/bot.js b/bot.js index 5894e2b..cada945 100644 --- a/bot.js +++ b/bot.js @@ -4,6 +4,9 @@ const path = require('path'); const { OpenRouterClient } = require('openrouter-kit'); const { getPrompt } = require('./prompts.js'); const logger = require('./logger.js'); +const callAI = require("./requestAI"); +const {searchInCache, searchInPrompts, saveInPrompts} = require("./promptResponser"); +const INSTRUCTIONS = require("./promptGen"); require('dotenv').config(); const char = {name:'marina'} class TelegramHistoryBot { @@ -66,7 +69,40 @@ class TelegramHistoryBot { } setupHandlers() { + this.bot.command('summy', async (ctx) => { + 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'); +console.log('получили отвкт: ', typeof request, request) + if (typeof request === 'string') { + try { + request = JSON.parse(request); + } catch { + request = {persona: "Карл Маркс", messages: 22, hours: 0}; + } + } +console.log('ищем персону') + 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 для запроса') + } + //handleSummaryCommand + 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); + } + //await ctx.reply(prompt) + + }); // Команды суммаризации - должны быть ДО обработки обычных сообщений this.bot.command('summary_day', async (ctx) => { // if (!this.isAdmin(ctx)) { @@ -220,10 +256,11 @@ class TelegramHistoryBot { return Object.keys(info).length > 0 ? info : null; } - async handleSummaryCommand(ctx, type, value) { + async handleSummaryCommand(ctx, type, value, options = {}) { logger.info(`📊 Обработка команды суммаризации: ${type}${value ? ` (${value})` : ''}`); const chatId = ctx.chat.id; + let messages = []; try { @@ -264,7 +301,7 @@ class TelegramHistoryBot { } // Генерируем суммаризацию - const summary = await this.generateSummary(messages, type, value); + const summary = await this.generateSummary(messages, type, value, options); // Отправляем результат await ctx.reply(summary, { parse_mode: 'HTML' }); @@ -298,11 +335,15 @@ class TelegramHistoryBot { .slice(-count); } - async generateSummary(messages, type, value) { + async generateSummary(messages, type, value, options = {}) { const preparedData = this.prepareDataForAI(messages); - const prompt = this.createSummaryPrompt(type, value); + if (options) { - return await this.callAISummarization(prompt, preparedData); + return await this.callAISummarization('Ты получишь данные чата. ' + options.promptToUse, preparedData); + } else { + const prompt = this.createSummaryPrompt(type, value); + return await this.callAISummarization(prompt, preparedData); + } } prepareDataForAI(messages) { @@ -440,79 +481,10 @@ ${participantsList} ХОД РАЗГОВОРА: ${conversationFlow}`; } - async callAISummarization(prompt, data) { - // Если API ключ недоступен, используем fallback - if (!process.env.OPENROUTER_API_KEY) { - logger.warn('⚠️ OpenRouter API ключ недоступен, используем базовую суммаризацию'); - return this.generateFallbackSummary(data); - } - - try { - const formattedMessages = this.formatMessagesForAI(data); - logger.info('🤖 Отправляем запрос к 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: process.env.OPENROUTER_MODEL, - // 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('Пустой ответ от ИИ'); - } - - logger.info('✅ Получен ответ от OpenRouter'); - return `🎬 История чата\n\n${aiSummary}`; - - } catch (error) { - logger.error('❌ Ошибка при вызове OpenRouter API:', error); - - // Детальная обработка различных типов ошибок - if (error.message.includes('401')) { - logger.error('❌ Неверный API ключ OpenRouter'); - } else if (error.message.includes('429')) { - logger.error('❌ Превышен лимит запросов OpenRouter'); - } else if (error.message.includes('402')) { - logger.error('❌ Недостаточно средств на счету OpenRouter'); - } else if (error.code === 'ENOTFOUND') { - logger.error('❌ Проблемы с сетевым подключением'); - } - - return this.generateFallbackSummary(data); - } + return await callAI(prompt, this.formatMessagesForAI(data), 'history') } + generateFallbackSummary(data) { const { metadata } = data; const mostActive = metadata.most_active_user; diff --git a/commandResponser.js b/commandResponser.js new file mode 100644 index 0000000..7650659 --- /dev/null +++ b/commandResponser.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const logger = require('./logger'); +const cacheFile = 'commandCache.json'; + +let aiCache = {}; + +// Загружаем кэш из файла при старте +function loadAiCache() { + logger.info('Загрузка командного кэша...'); + try { + if (fs.existsSync(cacheFile)) { + const data = fs.readFileSync(cacheFile); + aiCache = JSON.parse(data); + logger.info('✅ Кэш команд загружен.'); + } else { + fs.writeFileSync(cacheFile, JSON.stringify({}, null, 2)); + logger.info('✅ Кэш команд создан.'); + } + } catch (error) { + logger.error('❌ Ошибка загрузки командного кэша:', error); + } +} + +// Сохраняем кэш в файл +function saveAiCache() { + try { + fs.writeFileSync(cacheFile, JSON.stringify(aiCache, null, 2)); + } catch (error) { + logger.error('❌ Ошибка сохранения командного кэша:', error); + } +} +loadAiCache(); + +// Поиск запроса в кэше +function searchInCache(request) { + logger.info(`🔎 Поиск в кэше: ${request}`); + if (!request || typeof request !== 'string') return null; + const result = aiCache[request]; + if (result) { + logger.info(`🔁 Найдено в кэше: ${request}`); + return result; + } + return null; +} + +// Сохранение запроса и ответа в кэш +// async function saveInCache(request, result) { +// if (!request || !result) return; +// aiCache[request] = result; +// saveAiCache(); +// logger.info(`💾 Запрос сохранён в кэш: ${request}`); +// } +async function saveInCache(request, result) { + if (!request || !result) return; + + let finalResult = result; + + // Если это результат парсинга команды и он пришёл как строка JSON + if (typeof result === 'string' && result.includes('"persona"') && result.includes('"messages"')) { + try { + finalResult = JSON.parse(result); + } catch (error) { + logger.warn(`⚠️ Не удалось распарсить JSON команды: ${error.message}`); + } + } + + aiCache[request] = finalResult; + saveAiCache(); + logger.info(`💾 Запрос сохранён в кэш: ${request}`); +} +// +// // Загрузим кэш при старте +// loadAiCache(); + +module.exports = { searchInCache, saveInCache }; diff --git a/promptGen.js b/promptGen.js new file mode 100644 index 0000000..fa4553c --- /dev/null +++ b/promptGen.js @@ -0,0 +1,73 @@ +const INSTRUCTIONS = `Ты генератор промптов для создания суммаризаторов чата от лица различных персонажей. + +ВХОДНЫЕ ДАННЫЕ: +Пользователь укажет персонажа одним из способов: +1. Известная личность: "Владимир Ленин", "Шерлок Холмс", "Дональд Трамп", "группа Металлика" +2. Описанный персонаж: "участник чата @username, его особенности - любит игуан, ебет медведей, пьет спирт, всех ненавидит, кроме тех, кто кидает мемасики" + +ЗАДАЧА: +На основе указанного персонажа создай полный промпт для суммаризатора чата. + +СТРУКТУРА ПРОМПТА: + +ТВОЙ ПЕРСОНАЖ - [Имя и краткое описание персонажа, его возраст/статус если релевантно] + +ЗАДАЧА: Создай ЖИВУЮ ИСТОРИЮ того, что происходило в чате [с точки зрения этого персонажа]. + +ВВОДНЫЕ: +- чат не начался, он продолжается много лет +- [специфичные для персонажа вводные] + +НЕ пиши сухие факты! Покажи [СТИЛЬ ПОВЕСТВОВАНИЯ ПЕРСОНАЖА]: +- Кто с кем общался и о чем +- Какие были споры, шутки, обсуждения +- [специфичные пункты для персонажа] + +СТИЛЬ: +- [Характерный стиль речи персонажа] +- [Специфичная лексика и обороты] +- [Особенности мировоззрения] +- [Отношение к людям и событиям] + +ВАЖНО: +- Мемы/медиа - [как персонаж это воспринимает] +- Споры - [как персонаж видит конфликты] +- НЕ используй теги пользователей, НЕ используй форматирование +- Длина: 2-4 абзаца максимум + +ОЧЕНЬ ВАЖНО: +- Игнорируй словесные игры +- Игнорируй рекламу +- [специфичные ограничения для персонажа] + +Заголовок НЕ НУЖЕН. + +[Финальная фраза в стиле персонажа, призывающая к анализу] + +ПРАВИЛА ГЕНЕРАЦИИ: + +1. Для ИЗВЕСТНЫХ ЛИЧНОСТЕЙ: +- Используй их реальные черты характера, манеру речи, мировоззрение +- Адаптируй их под современные реалии чата +- Сохраняй узнаваемость персонажа + +2. Для ИЗВЕСТНЫХ ПИСАТЕЛЕЙ: +- Используй их реальные черты характера, манеру речи, мировоззрение +- Текст должен быть похож на часть книги, написанной автором. +- Сохраняй узнаваемость стилистики автора + + +3. Для ОПИСАННЫХ ПЕРСОНАЖЕЙ: +- Строго следуй данному описанию +- Развивай характер на основе указанных особенностей +- Не придумывай лишних черт, не упомянутых в описании + +4. ОБЩИЕ ТРЕБОВАНИЯ: +- Персонаж должен быть ярким и узнаваемым +- Стиль речи должен отличаться от других персонажей +- Избегай повторов конструкций из предыдущих промптов +- Делай персонажа живым, а не картонным + +ВАЖНО: Не используй в промпте форматирование и теги пользователей!` + +module.exports = INSTRUCTIONS \ No newline at end of file diff --git a/promptResponser.js b/promptResponser.js new file mode 100644 index 0000000..3cfdab9 --- /dev/null +++ b/promptResponser.js @@ -0,0 +1,105 @@ +const fs = require('fs'); +const logger = require('./logger'); +const cacheFile = 'prompts.json'; + +let prompts = {}; + +// Загружаем кэш из файла при старте +function loadPrompts() { + logger.info('Загрузка промптов...'); + try { + if (fs.existsSync(cacheFile)) { + const data = fs.readFileSync(cacheFile); + prompts = JSON.parse(data); + logger.info('✅ Промпты загружены.'); + } else { + fs.writeFileSync(cacheFile, JSON.stringify({}, null, 2)); + logger.info('✅ Файл промптов создан.'); + } + } catch (error) { + logger.error('❌ Ошибка загрузки промптов:', error); + } +} + +// Сохраняем кэш в файл +function savePrompt() { + try { + fs.writeFileSync(cacheFile, JSON.stringify(prompts, null, 2)); + } catch (error) { + logger.error('❌ Ошибка сохранения промптов:', error); + } +} +loadPrompts(); + +// Поиск запроса в кэше + +/** + * Выполняет поиск промпта в кэше по ключу запроса + * + * @description Функция ищет сохранённый промпт в глобальном кэше prompts. + * Логирует процесс поиска и результат. Используется для избежания повторных + * обращений к AI при одинаковых запросах. + * + * @param {string} request - Ключ запроса для поиска в кэше промптов + * @returns {string|null} Найденный промпт или null, если не найден + * + * @example + * // Поиск существующего промпта + * const prompt = searchInPrompts("Карл Маркс"); + * if (prompt) { + * console.log("Промпт найден:", prompt); + * } + * + * @example + * // Обработка отсутствующего промпта + * const result = searchInPrompts("Журавль летающий"); + * // result будет null + * + * @since 1.0.0 + * @author Vufer + * + * @see {@link saveInPrompts} для сохранения в кэш + * + * @throws {Error} Не выбрасывает исключений, возвращает null при ошибках + * + * @todo Добавить поиск по частичному совпадению ключей + */ +function searchInPrompts(request) { + logger.info(`🔎 Поиск в промптах: ${request}`); + if (!request || typeof request !== 'string') return null; + const result = prompts[request] || null; + if (result) { + logger.info(`🔁 Найдено в промптах: ${request}`); + return result; + } + return null; +} +// Сохранение запроса и ответа в кэш +// async function saveInCache(request, result) { +// if (!request || !result) return; +// aiCache[request] = result; +// saveAiCache(); +// logger.info(`💾 Запрос сохранён в кэш: ${request}`); +// } +async function saveInPrompts(request, result) { + if (!request || !result) return; + + let finalResult = result; + + if (typeof result === 'string' && result.includes('persona') && result.includes('messages')) { + try { + finalResult = JSON.parse(result); + } catch (error) { + logger.warn(`⚠️ Не удалось распарсить JSON команды: ${error.message}`); + } + } + + prompts[request] = finalResult; + savePrompt(); + logger.info(`💾 Промпт сохранён в кэш: ${request}`); +} +// +// // Загрузим кэш при старте +// loadAiCache(); + +module.exports = { searchInPrompts, saveInPrompts }; diff --git a/requestAI.js b/requestAI.js new file mode 100644 index 0000000..185daba --- /dev/null +++ b/requestAI.js @@ -0,0 +1,218 @@ +const logger = require("./logger"); +const {searchInCache, saveInCache} = require("./commandResponser"); +require('dotenv').config(); +// request_parse_prompt= 'Получена строка, это команда ТГ бота. ' + +// 'Команда может содержать период или количество сообщений, а так же имя персонажа (вероятнее всего известная личность, возможно - название музыкальной группы).' + +// 'Необходимо разобрать.' + +// 'Если не удалось разобрать, то вернуть со значениями persona:"Карл Маркс",messages:22.' + +// 'Одновременно не может быть и количество сообщений, и количество часов, что-то одно.' + +// 'Если интервал не в часах - пересчитать в часы!' + +// 'Если имя принадлежит публичной или известной персоне - указать имя полностью! (например - кипелов - Валерий Кипелов, кюри - Мария Склодовская-Кюри' + +// 'Если это название музыкальной группы - указать, что это музыкальная группа (группа ХХХХ)' + +// 'Комментарии не оставлять' + +// 'Вывод - строго в формате json' + +// 'command={\n' + +// ' persona:\'Карл Маркс\', // имя персонажа\n' + +// ' messages:100, // сколько сообщений обработать или 0\n' + +// ' hours: 24 // за сколько часов, или 0\n' + +// '}' ; +// +// +request_parse_prompt= 'Ты - эксперт по анализу текстовых команд. Твоя задача - разобрать команду Telegram бота и извлечь из неё информацию о персоне и временном периоде.\n' + + 'ВХОДНЫЕ ДАННЫЕ\n' + + 'Получена строка команды, которая может содержать:\n' + + '\n' + + 'Имя персонажа (известная личность, историческая фигура, или название музыкальной группы)\n' + + 'Временной период (количество часов, дней, недель, месяцев) ИЛИ количество сообщений\n' + + 'Дополнительные слова и предлоги\n' + + '\n' + + 'ПРАВИЛА ОБРАБОТКИ\n' + + '1. ПЕРСОНА\n' + + '\n' + + 'Если упомянута известная личность - указать полное имя (например: "кипелов" → "Валерий Кипелов", "кюри" → "Мария Склодовская-Кюри", "летов" → "Егор Николаевич Летов")\n' + + 'Если это музыкальная группа - указать с пометкой "группа" (например: "битлз" → "группа The Beatles", "гражданская оборона" → "группа Гражданская оборона")\n' + + 'Если персона не определена или неясна - использовать "Карл Маркс"\n' + + 'Учитывать различные формы написания (сокращения, транслитерацию, опечатки)\n' + + '\n' + + '2. ВРЕМЕННОЙ ПЕРИОД\n' + + '\n' + + 'Может быть указан в различных форматах: "2 часа", "3ч", "день", "неделю", "месяц", "за вчера"\n' + + 'ВСЕ периоды пересчитывать в ЧАСЫ:\n' + + '\n' + + '1 день = 24 часа\n' + + '1 неделя = 168 часов\n' + + '1 месяц = 720 часов (30 дней)\n' + + '"вчера" = 24 часа\n' + + '"позавчера" = 48 часов\n' + + '\n' + + '\n' + + 'Если период не указан - использовать 0\n' + + '\n' + + '3. КОЛИЧЕСТВО СООБЩЕНИЙ\n' + + '\n' + + 'Может быть указано как число + "сообщений", "постов", "записей"\n' + + 'Если не указано - использовать 0\n' + + 'ВАЖНО: одновременно НЕ МОЖЕТ быть и количество сообщений, и временной период - только что-то одно!\n' + + '\n' + + '4. ПРИОРИТЕТЫ\n' + + '\n' + + 'Если указаны и период, и количество сообщений - приоритет у количества сообщений (hours = 0)\n' + + 'Если ничего не указано - messages = 22, hours = 0\n' + + '\n' + + 'ПРИМЕРЫ ВХОДНЫХ КОМАНД И ОЖИДАЕМЫЙ РЕЗУЛЬТАТ\n' + + 'Команда: "покажи последние 50 сообщений в стиле Высоцкого"\n' + + 'Результат: {"persona": "Владимир Семёнович Высоцкий", "messages": 50, "hours": 0}\n' + + 'Команда: "сгенерируй как Metallica за последний день"\n' + + 'Результат: {"persona": "группа Metallica", "messages": 0, "hours": 24}\n' + + 'Команда: "в стиле Маска за 3 часа"\n' + + 'Результат: {"persona": "Илон Маск", "messages": 0, "hours": 3}\n' + + 'Команда: "как Цой последние 100 постов"\n' + + 'Результат: {"persona": "Виктор Робертович Цой", "messages": 100, "hours": 0}\n' + + 'Команда: "покажи Бетховена"\n' + + 'Результат: {"persona": "Людвиг ван Бетховен", "messages": 22, "hours": 0}\n' + + 'ИНСТРУКЦИИ ПО ВЫВОДУ\n' + + '\n' + + 'Комментарии НЕ оставлять\n' + + 'Вывод СТРОГО в формате JSON без markdown блоков\n' + + 'НЕ ИСПОЛЬЗОВАТЬ json или блоки\n' + + 'Возвращать ТОЛЬКО чистый JSON объект\n' + + 'Не добавлять дополнительные поля\n' + + 'Использовать двойные кавычки для JSON\n' + + 'Никаких объяснений до или после JSON\n' + + 'ОБЯЗАТЕЛЬНО ВАЛИДНЫЙ JSON!!! ОЧЕНЬ ВАЖНО!!\n' + + '\n' + + 'ФОРМАТ ОТВЕТА (возвращать БЕЗ markdown блоков):\n' + + '{"persona": "Полное имя персоны или группа Название (краткое описание в трех-пяти словах)", "messages": число_или_0, "hours": число_или_0}\n' + + 'КРИТИЧЕСКИ ВАЖНО\n' + + '\n' + + 'Твой ответ должен начинаться с { и заканчиваться }\n' + + 'Никаких дополнительных символов, пробелов или переносов строк до и после JSON\n' + + 'НЕ оборачивать в ```json блоки\n' + + 'Возвращать ТОЛЬКО валидный JSON объект\n' + + '\n' + + 'ОБРАБАТЫВАЙ КОМАНДУ СОГЛАСНО ВСЕМ ПРАВИЛАМ ВЫШЕ. АНАЛИЗИРУЙ КАЖДОЕ СЛОВО И КОНТЕКСТ. ВЕРНИ ТОЛЬКО JSON!' + + +// async function handleRequest(request) { +// // Попытка найти результат в кэше +// const cachedResult = await searchInCache(request); +// +// if (cachedResult) { +// return cachedResult; // Возвращаем найденное в кэше +// } +// +// // Если в кэше ничего нет — вызываем основную функцию +// const result = await someFunction(request); +// +// // Сохраняем результат в кэш +// await saveInCache(request, result); +// +// return result; +// } + +async function callAI(prompt, data, type = 'history') { + // Если API ключ недоступен, используем fallback + if (!process.env.OPENROUTER_API_KEY ) { + logger.error('⚠️ OpenRouter API ключ недоступен'); + return; + } + let workingPrompt = ''; + switch (type) { + case 'history':workingPrompt=prompt; + break; + case 'request':{ + workingPrompt=request_parse_prompt; + const cachedResult = await searchInCache(data); + if (cachedResult) { + return cachedResult; // Возвращаем найденное в кэше + } + } + + break; + default:workingPrompt=prompt; + break; + } + try { + const formattedMessages = data; + // const formattedMessages = this.formatMessagesForAI(data); + let useModel + switch (type) { + case 'history':useModel=process.env.OPENROUTER_MODEL; + break; + case 'prompt':useModel=process.env.OPENROUTER_MODEL; + break; + case 'request':useModel=process.env.OPENROUTER_CHEAP_MODEL; + break; + default:useModel=process.env.OPENROUTER_MODEL; + break; + } + // = (type!==='history' ? process.env.OPENROUTER_MODEL: process.env.OPENROUTER_CHEAP_MODEL); + logger.info(`🤖 Отправляем запрос к OpenRouter (модель ${useModel})...`); + 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://git.rockzo.ru/Rockzo_Develop/summy', + 'X-Title': 'Telegram History Bot' + }, + body: JSON.stringify({ + model: useModel, + messages: [ + { + role: 'system', + content: workingPrompt + }, + { + 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) { + logger.error(JSON.stringify(completion)); + throw new Error('Пустой ответ от ИИ'); + } + + logger.info('✅ Получен ответ от OpenRouter'); + if (type==='request') { + await saveInCache(data, aiSummary); + } + // return `🎬 История чата\n\n${aiSummary}`; +if (type==='request') { + console.log(typeof aiSummary, aiSummary) + return JSON.parse(aiSummary); +} + return aiSummary; + + } catch (error) { + logger.error('❌ Ошибка при вызове OpenRouter API:', error); + + // Детальная обработка различных типов ошибок + if (error.message.includes('401')) { + logger.error('❌ Неверный API ключ OpenRouter'); + } else if (error.message.includes('429')) { + logger.error('❌ Превышен лимит запросов OpenRouter'); + } else if (error.message.includes('402')) { + logger.error('❌ Недостаточно средств на счету OpenRouter'); + } else if (error.code === 'ENOTFOUND') { + logger.error('❌ Проблемы с сетевым подключением'); + } + + return // this.generateFallbackSummary(data); + } +} +module.exports = callAI; \ No newline at end of file