openspec: архив 14 завершённых change-ов (веб-фиксы, crawler-queue, own-content-hub, publisher-service); спеки влиты в openspec/specs

This commit is contained in:
kpa39l
2026-09-16 17:01:00 +00:00
parent 584582a48c
commit 771f6a8276
88 changed files with 1632 additions and 3 deletions
@@ -0,0 +1,90 @@
# Spec: rss-crawler
## Purpose
Краулер RSS/Atom-лент с реестра источников (sources.yaml, crawler: rss): условные
GET (etag/modified), нормализация URL, извлечение полного текста (trafilatura),
состояние и здоровье ленты в БД (rss_state), интеграция с posts/runs. RSS-посты
попадают в общий конвейер (классификация → кандидаты → публикация) наравне с
Telegram-постами.
## ADDED Requirements
### Requirement: Чтение RSS-источников из реестра
Система MUST поддерживать источники с `crawler: rss` в sources.yaml и запускать
их краулинг через `python -m crawler.rss_crawler` (фильтры: --all / --direction /
--source <slug>). Каждый запуск источника MUST записываться в таблицу `runs`
(start_run/finish_run) как задача `rss:<slug>`.
#### Scenario: Список RSS-источников
- **GIVEN** sources.yaml содержит источник `lwn` с `crawler: rss`, `enabled: true`
- **WHEN** выполняется `python -m crawler.rss_crawler --all`
- **THEN** краулер обрабатывает источник lwn, в `runs` появляется запись задачи `rss:lwn`
### Requirement: Условные GET (etag/modified)
Краулер MUST отправлять `If-None-Match` (etag) и `If-Modified-Since` (modified) из
`rss_state` при запросе ленты. При ответе 304 краулер MUST НЕ парсить ленту и НЕ
добавлять посты (запуск завершается ok с 0 fetched/0 new). Полученные etag/modified
из ответа MUST сохраняться в `rss_state`.
#### Scenario: Неизменённая лента
- **GIVEN** rss_state для lwn содержит etag "xyz" и лента не менялась
- **WHEN** запускается краулер
- **THEN** сервер отвечает 304, новых постов нет, запуск в runs имеет status ok
### Requirement: Нормализация URL и дедупликация
Краулер MUST нормализовать URL записи перед использованием в качестве ключа:
удалять UTM-параметры (utm_*), fbclid/gclid/ref и якоря (#). Дедупликация MUST
использовать sha256 от текста записи (как в telegram_crawler); повторная вставка
той же статьи MUST не создавать второй строки в `posts`.
#### Scenario: Дубликат: та же статья, разные URL
- **GIVEN** статья с URL вида `https://site/a?utm_source=rss&utm_medium=feed#top`
- **WHEN** краулер обрабатывает запись
- **THEN** canonical_url = `https://site/a` (без утм и якоря), sha256 уникален,
вторая идентичная запись из другой ленты не создаёт дубль
### Requirement: Полный текст (trafilatura)
Краулер MUST извлекать полный текст страницы через trafilatura для записей,
у которых нет полного текста в ленте (только summary). Сбой извлечения MUST НЕ
ронять источник: в `posts.text` сохраняется доступный текст (summary), запуск
продолжается.
#### Scenario: Summary-only лента
- **GIVEN** лента отдаёт краткое описание (summary) без полного контента
- **WHEN** краулер обрабатывает запись
- **THEN** текст записи дотягивается trafilatura; при неудаче сохраняется summary,
запуск завершается ok, ошибка логгируется
### Requirement: Здоровье и состояние ленты (rss_state)
Система MUST хранить состояние ленты в таблице `rss_state` (slug PK, etag, modified,
last_build_date, last_error, error_count, status). После успешного прогона
`status='alive'`, error_count=0, last_error=NULL. При ошибке error_count
инкрементируется; при error_count>=5 подряд `status='dead'` (фид-здоровье),
sources.status синхронизируется.
#### Scenario: Лента постоянно падает
- **GIVEN** лента возвращает 500/таймаут 5 раз подряд
- **WHEN** краулер завершает 5-й неудачный запуск
- **THEN** rss_state.status='dead', sources.status='dead', last_error непуст;
watchdog (cron) уведомит при переходе alive→dead
### Requirement: Запуск по приоритетам и CLI
Краулер MUST запускаться из cron с каденцией по приоритету источника (P0 раз в
15 мин, P1 раз в час, P2 раз в 4 часа). Ручной запуск MUST поддерживать
`--dry-run` (печать записей без записи в БД).
#### Scenario: Dry-run
- **GIVEN** выполняется `python -m crawler.rss_crawler --source lwn --dry-run`
- **THEN** краулер печатает записи ленты, но НЕ пишет в posts/rss_state/runs;
БД остаётся неизменной
## NOT Requirements
- Не реализуем инкрементальный обход истории (как telegram_crawler по last_post_id):
RSS-ленты — это «последние N записей», состояние — через etag/modified.
- Не пишем полный контент в отдельную таблицу/колонку (text в posts уже достаточен;
bundles создаёт веб при approve).
- Не скачиваем медиа из RSS (картинки-иконки ленты) — контент текстовый.
- Не делаем автопостинг: RSS-посты проходят тот же путь подтверждения человеком.
- Не затрагиваем tg_state/telegram_crawler (отдельный механизм для Telegram).