Zero-server новостник на Python: запускаем автономного ИИ-редактора в GitHub Actions с нулевым бюджетом
Обычно создание Telegram-канала с автоматическим постингом новостей выглядит одинаково: пишется скрипт на feedparser, арендуется копеечный VPS, настраивается Cron и раз в час в канал падает безликая простыня текста со ссылкой. На выходе получается очередная спам-помойка, от которой юзеры отписываются через день.
Я решил собрать проект «на максималках», решив три проблемы:
- Качество контента. Бот должен не просто парсить всё подряд, а фильтровать мусор, переводить, сжимать длинные статьи до 3–5 емких предложений и подбирать адекватный визуал.
- Частота постов. Никакого спама каждый час. Канал должен работать в режиме медиа: редкие точечные посты + структурированный дайджест по утрам и вечерам.
- Цена инфраструктуры. Стоимость хостинга, баз данных и API для обработки текста должна быть ровно 0 долларов.
В итоге получился автономный скрипт на Python, который живет прямо в GitHub Actions, сохраняет состояние в сам Git-репозиторий и использует бесплатные лимиты LLM.
Ниже подробно о том, как устроен пайплайн и какие грабли пришлось собрать в процессе.
Архитектурный финт: База данных в Git и Zero-Server
Главная проблема при отказе от постоянного сервера (VPS) — где хранить состояние? Бот должен помнить, какие статьи он уже опубликовал, и на каком источнике прервался прошлый цикл обхода (round-robin).
Я решил не поднимать облачные БД, а использовать файловую систему самого гит-репозитория как персистентное хранилище:
- Текущие позиции курсора по RSS-лентам пишутся в data/state.json.
- Хэши и URL отработанных статей складываются в обычный текстовый файл data/seen_urls.txt(защита от дубликатов).
- Скрипт отрабатывает на виртуальной машине GitHub Actions (используется бесплатный лимит в 2000 минут/месяц).
- В самом конце успешного воркфлоу встроен шаг, который делает git commit измененных файлов из папки data/ и пушит их обратно в main-ветку.
Для этого в настройках репозитория (Settings → Actions → General → Workflow permissions) достаточно выдать раннеру права на запись (Read and write permissions). Чтобы избежать конфликтов при одновременном доступе, воркфлоу жестко ограничен через параметр concurrency.group: post-news.
Внешний костыль для запуска
Встроенный планировщик GitHub Actions (cron) — вещь крайне ненадежная. Слот времени может опоздать на пару часов или вообще молча пропустить запуск.
Проблема решилась связкой workflow_dispatch и внешнего бесплатного сервиса cron-job.org. Раз в час сторонний сервис отправляет POST-запрос с токеном авторизации на API GitHub, принудительно и вовремя толкая раннер. Один прогон занимает чуть больше минуты, так что лимитов Actions хватает с огромным запасом.
Пайплайн обработки: от RSS до Telegram-поста
Бот собирает пул из 5 свежих статей из конфига. Чтобы отсеять неинтересный проходняк, эти 5 кандидатов (только заголовки и короткие описания) скармливаются модели на OpenRouter (на бесплатном тарифе отлично заходят модели вроде nvidia/nemotron).
Prompt заставляет модель проанализировать список и вернуть строго один ID статьи, которая потенциально наиболее интересна IT-аудитории. Если нейросеть ломается, выдает мусор или не попадает в регулярное выражение — срабатывает фолбэк (fallback) на стандартную очередь: берется первая рабочая ссылка из списка.
1.Выкачивание «мяса» и сжатие
По ссылке-победителю бот забирает «голый» текст страницы без рекламы и сайдбаров с помощью библиотеки trafilatura. Весь массив текста снова уходит в LLM.
Одним запросом модель решает сразу три задачи:
- Понимает язык (если это TechCrunch — переводит, если Хабр — оставляет как есть).
- Выдает хлесткий заголовок и выжимку строго в 3–5 предложений.
- Формирует короткий лаконичный промпт на английском для генерации визуала.
2. Отказоустойчивый конвейер графики
Пост без картинки в телеге не читают. Но внешние сервисы генерации часто выдают таймауты. Я написал цепочку из четырех фолбэков в main.py:
- Шаг 1: Бот ищет тег картинки в самом RSS-элементе.
- Шаг 2: Если там пусто, вытаскивает og:image из мета-тегов страницы во время парсинга (без повторного запроса к сайту).
- Шаг 3: Если сайт закрыт от парсинга картинок, берется API бесплатного стока Pexels и ищет фото по ключевым словам новости.
- Шаг 4: Крайний рубеж — отправка сгенерированного ИИ промпта в Pollinations.ai (работает без токенов и ключей).
Если падает вообще всё — пост уходит чистым текстом через publisher.py. Бот качает картинку напрямую в память и шлет в Telegram в виде байт. Если слать просто ссылкой, Telegram на медленных прокси часто обрезает превью, и пост выглядит убого.
Пересборка логики: Почему публикации раз в час — это плохо
Изначально бот молотил каждый час и сразу выплевывал результат в канал. Спустя сутки тестов стало понятно: это порождает жуткий информационный шум. 15-20 постов в день превращают канал в спам-бота, юзеры моментально жмут кнопку Mute.
Я переделал логику распределения контента (main.py):
- Дневной режим: Бот запускается каждый час, но лимит на отправку (MAX_ARTICLES_PER_RUN) зажат. В итоге в канал уходит всего 3–4 самых важных и отфильтрованных события за день.
- Режим Дайджеста: Ровно в 08:00(по Минску/Москве) бот переключается на ветку digest.py. Он идет в пул мировых медиа (BBC, CNN, Guardian), собирает повестку, агрегирует данные и выдает один структурированный пост-дайджест по 5 главным технологическим и научным категориям. Это закрывает потребность «быть в курсе событий», не забивая ленту уведомлениями.
Что по деньгам?
Стек полностью бесплатный, без необходимости привязывать зарубежные карты для триал-периодов:
- Хостинг скрипта: GitHub Actions (0$).
- Триггер таймингов: Cron-job.org (0$).
- Текст и перевод: OpenRouter Free API (квоты в ~50 тяжелых запросов в сутки хватает впритык на 16 сессий).
- Медиа: Лимиты Pexels API + Pollinations.ai (0$).
Исходный код
Код полностью открыт, архитектура модульная — при желании пайплайн можно пересобрать под любую другую нишу (крипта, гаджеты, региональные новости), просто поменяв RSS-фиды в config.py.
- Репозиторий на GitHub: Репозиторий
- Посмотреть на живой результат работы автоматики: Технарь
Буду рад фидбеку в комментариях. Что думаете по поводу идеи использовать коммиты в Git вместо полноценной базы данных для пет-проектов? Какие подводные камни я мог упустить?