# Proposal: rss-crawler ## Why В VESTI сейчас есть только telegram_crawler (Telethon, MTProto). RSS-источники — самый массовый и дешёвый класс источников (по оценке PRD — ~80% объёма новостей): веб-СМИ, блоги, IT-порталы публикуют RSS/Atom-ленты. Без RSS-краулера они недоступны. Проблемы, которые решает модуль: - **нет спроса на RSS вовсе**: в источники (sources.yaml) нельзя добавить ни одного RSS-канала — краулер просто не умеет их обрабатывать (`get_enabled_sources(crawler="rss")` вернёт пусто); - **негражданный режим опроса**: без If-None-Match/If-Modified-Since каждый запуск тянет полные ленты — это лишний трафик для издателей и для нас; - **мусорные ссылки**: URL из лент часто содержат UTM-метки, якоря, трекеры — дедупликация по сырому URL даёт дубли одной статьи; - **обрывки вместо текста**: многие ленты отдают summary вместо полного текста — без дотягивания полного текста карточки кандидатов бедные; - **нет состояния/здоровья**: нет etag/modified/last_error/status для RSS-источников. Для пользователя это значит: можно добавить в VESTI новостные сайты/блоги и получать их посты в конвейере (классификация → кандидаты → публикация) наравне с Telegram-каналами. ## What Changes - Новый модуль краулера `crawler/rss_crawler.py` (feedparser + httpx): - читает `sources.yaml` (crawler: rss), группирует по priority; - HTTP GET c `If-None-Match`/`If-Modified-Since` из таблицы `rss_state` → 304 = пропуск; - нормализует URL (UTM-параметры, якоря) → `canonical_url`, дедуп по sha256; - извлекает полный текст через trafilatura, если запись — обрывок; - пишет посты в `posts` через существующий `store_posts`-механизм (дедуп по sha256 текста); - пишет метаданные ленты в `rss_state` (etag/modified/last_error/status, last_build_date); - ведёт `runs` (source_id, task, trigger) — как telegram_crawler; - CLI: `python -m crawler.rss_crawler --all | --direction | --source `. - Новая таблица `rss_state` (etag, modified, last_error, status, last_build_date) — аналог `tg_state` для RSS-лент. - `requirements.txt`: + trafilatura (извлечение полного текста). - `sources.yaml`: первый реальный RSS-источник для проверки — LWN (lwn.net) — и один закомментированный пример (opennet.ru). - `.env.example`: + `RSS_USER_AGENT` (информативный User-Agent). - Cron (Hermes): `vesti-rss-crawler` (каждые 15 мин, P0) — тихий, no_agent, как telegram. ### Не меняется - Дедуп и схема `posts` — как у telegram_crawler (sha256 текста; url — дополнительный ключ). - Классификатор, веб, publisher — не затрагиваются (RSS-посты идут в тот же `posts`). - `tg_state` — остаётся для Telegram; RSS использует `rss_state`. - Никаких автопубликаций: посты попадают в кандидаты, подтверждение — человеком. ## Capabilities ### New Capabilities - `rss-crawler`: Синхронный краулер RSS/Atom-лент: реестр из sources.yaml, условные GET (etag/modified), нормализация URL, извлечение полного текста (trafilatura), состояние и здоровье ленты в БД, интеграция с runs/posts. ### Modified Capabilities - `news-store` (таблица `posts`): без изменений схемы; RSS-посты используют её как есть. - `sources.yaml`: новые источники с `crawler: rss`. - `vesti-web`: без изменений (RSS-посты автоматически видны как кандидаты). ## Impact - Файлы: - новый: `crawler/rss_crawler.py` (модуль-краулер); - изменён: `db/schema.sql` (+CREATE TABLE rss_state), `requirements.txt` (+ trafilatura), `sources/sources.yaml` (+lwn, пример opennet), `.env.example` (+RSS_USER_AGENT), `config.py` (+RSS_USER_AGENT, +RSS_*); - доки: STATUS.md, TODO.md, WALKTHROUGH.md, AGENT.MD (команда запуска). - Данные: миграция — `CREATE TABLE IF NOT EXISTS`; существующие данные не трогаются. - Секреты: не требует (RSS публичные). User-Agent — из .env. - Сеть: исходящие HTTP-запросы к сайтам издателей (только к лентам, учтиво: условные GET, рейт-лимит 1–2 с между источниками). - Rollback: модуль не вызывается cron — просто не добавлять источники/не запускать. ## Risks - Издатели режут по User-Agent / отдают капчу — лечится `RSS_USER_AGENT` (честный, с контактом) и `enabled: false` для проблемных. - Ленты с кривой датой (`published_parsed` None) — ставим `fetched_at`, не падаем. - summary-only ленты — trafilatura может не найти полный текст; тогда сохраняем summary, пост всё равно попадает в конвейер (пометка в content_type/text). - Огромные ленты (мега-фиды): ограничение — первые N записей, остальные догонятся следующими запусками (инкрементально), как в telegram_crawler. - Троттлинг издателя: проставляем last_error и status='dead' после N=5 ошибок подряд (правило фид-здоровья), watchdog молчит до перехода alive↔dead.