Serverless-редактор в Telegram на базе Gemini и GitHub Actions: реализация за 0 рублей
Создание персонального новостного агрегатора часто упирается либо в стоимость готовых решений (Perplexity, ChatGPT Plus), либо в необходимость администрирования собственного хостинга (VPS). Однако для задач периодического мониторинга и суммаризации контента достаточно бесплатных инструментов: Google Gemini API (Free Tier) и GitHub Actions.
На реальном примере рассказываю про свой опыт и реализацию автономного бота, который работает без постоянного сервера, парсит RSS-ленты по расписанию, структурирует информацию с помощью LLM и публикует готовые посты в Telegram.
Архитектура решения
Классические Telegram-боты обычно работают в режиме polling (постоянный опрос сервера), что требует запущенного процесса 24/7. Для новостного агрегатора это избыточно. Оптимальный подход — CRON-скрипт.
Схема работы:
Scheduler: GitHub Actions запускает контейнер по расписанию (например, 4 раза в сутки).
Collector: Python-скрипт собирает заголовки из RSS-источников.
Processor: Gemini Flash обрабатывает сырые данные, фильтрует и формирует текст.
Publisher: Готовый контент отправляется через Telegram Bot API.
Termination: Контейнер уничтожается до следующего запуска.

Это позволяет уложиться в бесплатные лимиты GitHub Actions и Google AI Studio.
1. Временные слоты и конфигурация
Чтобы бот не превратился в спам-машину, логика вещания привязывается ко времени суток. Скрипт определяет текущий час и выбирает соответствующий профиль: утренний дайджест, дневной лонгрид или вечерняя аналитика.
В коде это реализуется через словарь конфигурации. Функция get_current_config возвращает целевые URL и режим промпта в зависимости от часа запуска.
def get_current_config():
hour = datetime.utcnow().hour
# Логика слотов (UTC): 6 утра, 10, 13 и 16 часов
target_slot = 6
if 9 <= hour < 12: target_slot = 10
elif 12 <= hour < 15: target_slot = 13
elif 15 <= hour < 20: target_slot = 16
CONFIG = {
6: { # Утро: короткий дайджест или рынки
"topic": "Главное к утру",
"mode": random.choice(["DIGEST", "MARKETS"]),
"urls": ["https://api.axios.com/feed/", "https://cointelegraph.com/feed"]
},
10: { # Обед: разбор технологии
"topic": "Технологии",
"mode": "ESSAY",
"urls": ["https://techcrunch.com/feed/", "https://www.theverge.com/rss/index.xml"]
}
# ... остальные слоты
}
return CONFIG[target_slot]
2. Агрегация данных
Для взаимодействия с LLM не обязательно скачивать полные тексты статей. Gemini обладает достаточным контекстным окном, но для экономии токенов и ускорения работы эффективнее скармливать модели список заголовков с ссылками.
Тут использую библиотеку feedparser. Собранные заголовки перемешиваются, чтобы избежать перекоса в сторону одного источника, если в ленте много новостей.
def get_combined_news(url_list):
all_entries = []
for url in url_list:
# Парсинг RSS и формирование строки вида [Источник] Заголовок
feed = feedparser.parse(requests.get(url).content)
for entry in feed.entries:
clean_link = entry.link.split("?")[0]
all_entries.append(f"- [{urlparse(clean_link).netloc}] {entry.title} ||| {clean_link}")
random.shuffle(all_entries)
return "\n".join(all_entries[:30]) # Ограничение объема на вход3. Инжиниринг промптов
Качество выхода напрямую зависит от жесткости заданных ограничений. Просто попросить «пересказать новости» приведет к потере фактуры. В скрипте используются разные системные промпты для разных режимов.
Ключевые требования к промпту:
Ролевая модель: «Ты редактор», «Ты финансовый аналитик».
Структура HTML: Telegram требует закрытых тегов. Модель инструктируется вставлять ссылки в формате
<a href='url'>источник</a>.Запрет на выдумки: Строгое следование переданному списку новостей.
def process_with_gemini(news_text, config):
mode = config['mode']
# Общие правила форматирования для всех режимов
SHARED_RULES = (
"ТОН: Спокойный, без маркетинга.\n"
"ССЫЛКИ: Встраивать в текст через тег <a href>.\n"
)
if mode == "ESSAY":
prompt = (
f"Роль: Редактор. Тема: {config['topic']}.\n"
"СТРУКТУРА (3 абзаца):\n"
"1. Заголовок (жирным).\n"
"2. Инфоповод: Что случилось + ссылка.\n"
"3. Контекст: Почему это важно.\n"
f"ЛЕНТА:\n{news_text}"
)
elif mode == "DIGEST":
prompt = "..." # Инструкция для списка событий
model = genai.GenerativeModel('gemini-flash-latest')
result = model.generate_content(prompt)
return result.text4. Взаимодействие с Telegram API
Telegram имеет ограничение на длину сообщения с медиа-вложением (caption) — 1024 символа. Если LLM сгенерирует длинный анализ, запрос sendPhoto вернет ошибку.
Реализована логика раздельной отправки:
Если текст короче 950 символов (оставляем запас на теги) — отправляется картинка с подписью.
Если текст длиннее — сначала отправляется изображение, следом идет текстовое сообщение.
Картинка парсится из OpenGraph-тегов исходной статьи (если режим подразумевает фокус на одной новости).
def send_to_telegram(raw_text, t_token, c_id):
# Логика определения длины
is_long_text = len(raw_text) > 950
if file_bytes and is_long_text:
# Раздельная отправка
requests.post(url_photo, files=..., data={'chat_id': c_id})
requests.post(url_message, data={'chat_id': c_id, 'text': raw_text})
elif file_bytes:
# Фото с подписью
requests.post(url_photo, files=..., data={'caption': raw_text})
else:
# Только текст
requests.post(url_message, data={'text': raw_text})
Финальный этап — автоматизация. Создается Workflow-файл .github/workflows/main.yml.
Секреты (API ключи Telegram и Google) хранятся в настройках репозитория (Settings -> Secrets). Крон настраивается со смещением минут (например, 15-я минута часа), чтобы избежать очередей на раннерах GitHub в начале часа.
name: News Bot
on:
schedule:
# Запуск в 06:15, 10:15, 13:15, 16:15 (UTC)
- cron: '15 6,10,13,16 * * *'
workflow_dispatch: # Ручной запуск
jobs:
run-bot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: pip install requests feedparser beautifulsoup4 google-generativeai
- name: Run Script
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHANNEL_ID: ${{ secrets.TELEGRAM_CHANNEL_ID }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
run: python main.pyЗаключение
Реализованная схема позволяет получить полностью бесплатный, необслуживаемый инструмент для мониторинга инфополя. Отсутствие состояния (stateless) упрощает отладку: каждый запуск происходит в чистом окружении. Ключевым преимуществом выстраивания пайплайна вокруг Google Gemini являются высокие лимиты Free Tier и широкое контекстное окно, достаточное для обработки массивных RSS-лент за одну итерацию.
Демонстрация работы описанного алгоритма в реальном времени доступна в Telegram-канале. https://t.me/explainer_news.
Спасибо, что дочитали.