mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 18:05:03 +00:00
openspec: архив 14 завершённых change-ов (веб-фиксы, crawler-queue, own-content-hub, publisher-service); спеки влиты в openspec/specs
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-15
|
||||
@@ -0,0 +1,170 @@
|
||||
# Design: rss-crawler
|
||||
|
||||
## Approach
|
||||
|
||||
Синхронный краулер RSS/Atom-лент, следующий общему паттерну telegram_crawler:
|
||||
реестр источников в `sources.yaml` → выборка включённых → запуск по каждому
|
||||
(с записью в `runs`) → нормализация → дедуп → вставка в `posts`.
|
||||
|
||||
Отличия от telegram_crawler:
|
||||
- транспорт — HTTP (httpx), а не MTProto;
|
||||
- состояние ленты — `rss_state` (etag/modified), а не `tg_state` (last_post_id);
|
||||
- условные GET (304 = пропуск) вместо инкрементального обхода истории;
|
||||
- извлечение полного текста (trafilatura) для summary-only записей;
|
||||
- данные ленты (summary/links/author) кладутся в текст записи, sha256 берётся
|
||||
с канонического текста — поэтому дубль статьи в двух лентах схлопнется.
|
||||
|
||||
Схема потока:
|
||||
```
|
||||
sources.yaml ──► rss_crawler.py ──► HTTP GET (If-None-Match/If-Modified-Since)
|
||||
│ 304 → skip (экономно)
|
||||
▼
|
||||
feedparser.parse
|
||||
│
|
||||
├─► нормализация URL (UTM/якоря) → canonical_url
|
||||
├─► трафилатура (полный текст) если summary-only
|
||||
▼
|
||||
posts (sha256-дедуп) + rss_state (etag/modified/status)
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
```bash
|
||||
# Новый модуль
|
||||
crawler/rss_crawler.py # краулер: CLI + run_source + fetch_feed + store
|
||||
|
||||
# Изменения
|
||||
db/schema.sql # + CREATE TABLE rss_state; sources + feed_url
|
||||
db/db.py # + миграция: ALTER TABLE sources ADD COLUMN feed_url (если нет)
|
||||
sources/sources.py # + синк feed_url в БД
|
||||
requirements.txt # + trafilatura
|
||||
sources/sources.yaml # + lwn (enabled), пример opennet (закомментирован)
|
||||
config.py # + RSS_USER_AGENT (env, default), RSS_DEFAULT_TIMEOUT
|
||||
.env.example # + RSS_USER_AGENT
|
||||
STATUS.md / TODO.md / WALKTHROUGH.md / AGENT.MD # статус и команды
|
||||
```
|
||||
|
||||
## Data / Config
|
||||
|
||||
```bash
|
||||
# .env (опционально, честный User-Agent с контактом)
|
||||
RSS_USER_AGENT=vesti-rss/0.1 (+https://vesti.nixg.ru)
|
||||
```
|
||||
|
||||
`sources.yaml` — формат записи (продолжает существующий):
|
||||
```yaml
|
||||
- slug: lwn
|
||||
name: "LWN.net"
|
||||
url: https://lwn.net/
|
||||
feed_url: https://lwn.net/headlines/rss
|
||||
channel: null # только для telegram
|
||||
crawler: rss # rss | telegram
|
||||
direction: linux # linux | tech | politics | games | electronics | llm
|
||||
lang: en
|
||||
priority: P0 # P0 (15 мин) | P1 (1 ч) | P2 (4 ч)
|
||||
enabled: true
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS sources (
|
||||
...
|
||||
feed_url TEXT, -- только для crawler: rss — URL ленты
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
Миграция существующей БД (CREATE IF NOT EXISTS не меняет таблицу):
|
||||
`db/db.py` init_db — после executescript проверить `PRAGMA table_info(sources)`,
|
||||
при отсутствии `feed_url` выполнить `ALTER TABLE sources ADD COLUMN feed_url TEXT`.
|
||||
`sources.py` sync_sources_to_db — добавить `feed_url` в INSERT/ON CONFLICT UPDATE.
|
||||
|
||||
## rss_state (новая таблица)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS rss_state (
|
||||
slug TEXT PRIMARY KEY, -- = sources.slug
|
||||
etag TEXT,
|
||||
modified TEXT,
|
||||
last_build_date TEXT,
|
||||
last_error TEXT,
|
||||
error_count INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'alive', -- alive | dead | new
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
## Алгоритм run_source
|
||||
|
||||
1. `start_run(source_id, f"rss:{slug}", trigger)`.
|
||||
2. Прочитать `rss_state` по slug; собрать заголовки `If-None-Match`/`If-Modified-Since`, если есть.
|
||||
3. `httpx.get(feed_url, headers={If-None-Match, If-Modified-Since, User-Agent}, timeout=20)`.
|
||||
4. `304` → `finish_run(ok, 0, 0)` (экономия трафика); не трогаем status.
|
||||
5. `200` → `feedparser.parse(resp.content)`:
|
||||
- если в контенте указано `etag`/`modified` — сохранить в rss_state;
|
||||
- для каждой записи (первые MAX_ITEMS=25, см. лимит ниже):
|
||||
- `canonical_url = normalize_url(entry.link)` (UTM/якоря),
|
||||
- `sha = sha256(canonical text)` (текст см. ниже),
|
||||
- `text = entry.summary или выжимка полного контента`,
|
||||
- если запись summary-only — попробовать полный текст через trafilatura
|
||||
(timeout, не ронять источник при сбое),
|
||||
- `published_at` из `published_parsed`/`updated_parsed` (dateutil) или None,
|
||||
- вставка через `store_rss_post` (дедуп: sha256 уникальна; url — доп. ключ).
|
||||
6. `finish_run(ok, fetched, new)`; `rss_state.status='alive'`, `last_error=NULL`,
|
||||
`error_count=0`; `last_build_date` из ленты.
|
||||
7. Ошибка сети/парсинга → `finish_run(error)`, `last_error`, `error_count+1`;
|
||||
при `error_count>=5` → `status='dead'`. `sources.status` (alive/dead) — как у TG.
|
||||
|
||||
### Текст записи (что идёт в posts.text)
|
||||
- Если есть `content[0].value` — раскрываем HTML в plain text (selectolax), берём его.
|
||||
- Иначе `summary` — раскрываем в plain text.
|
||||
- Если записи нет полного текста, а выжимка короткая (< 220 симв.) ИЛИ в ленте нет
|
||||
`content` вовсе — пробуем trafilatura по `canonical_url`.
|
||||
- Итог: `text` = полный текст (если дотянут) или выжимка; `content_type` = 'text'.
|
||||
- sha256 считаем от финального `text` (после нормализации пробелов) — это делает
|
||||
дедуп устойчивым к мелким различиям лент.
|
||||
|
||||
## Стандартизация URL (canonical_url)
|
||||
|
||||
```python
|
||||
def normalize_url(url):
|
||||
if not url: return url
|
||||
u = urlparse(url)
|
||||
q = parse_qsl(u.query, keep_blank_values=True)
|
||||
q = [(k, v) for k, v in q if k.lower() not in UTM_PARAMS] # utm_*, fbclid, gclid
|
||||
return urlunparse(u._replace(query=urlencode(q), fragment=""))
|
||||
```
|
||||
UTM_PARAMS = {utm_source, utm_medium, utm_campaign, utm_term, utm_content, fbclid, gclid, ref}.
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m crawler.rss_crawler --all # все включённые rss-источники
|
||||
.venv/bin/python -m crawler.rss_crawler --direction linux
|
||||
.venv/bin/python -m crawler.rss_crawler --source lwn
|
||||
.venv/bin/python -m crawler.rss_crawler --dry-run # не писать в БД, только печать
|
||||
```
|
||||
|
||||
## Команды
|
||||
|
||||
```bash
|
||||
cd /opt/vesti
|
||||
.venv/bin/pip install -r requirements.txt # + trafilatura
|
||||
.venv/bin/python -m crawler.rss_crawler --all --dry-run
|
||||
.venv/bin/python -m crawler.rss_crawler --source lwn
|
||||
# БД: проверить
|
||||
.venv/bin/python - <<'EOF'
|
||||
import sqlite3
|
||||
c = sqlite3.connect('db/vesti.db')
|
||||
print(c.execute("SELECT slug,status,error_count FROM rss_state").fetchall())
|
||||
print(c.execute("SELECT COUNT(*) FROM posts p JOIN sources s ON s.id=p.source_id WHERE s.crawler='rss'").fetchone())
|
||||
EOF
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] `openspec validate rss-crawler` → 0 ошибок
|
||||
- [ ] `--dry-run` против lwn: печатает ≥5 записей, ничего не пишет в БД
|
||||
- [ ] Реальный прогон `--source lwn`: posts.rss_count>0, в rss_state появились etag/last_build_date, status=alive
|
||||
- [ ] Повторный прогон: 304 (вторичный запуск) или 0 новых (если лента без etag); runs — ok
|
||||
- [ ] Повторная вставка той же записи (та же лента дважды) → новых 0, дублей 0
|
||||
- [ ] Веб-кандидаты показывают RSS-пост (с направлением после классификации)
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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 <slug>`.
|
||||
- Новая таблица `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.
|
||||
@@ -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).
|
||||
@@ -0,0 +1,48 @@
|
||||
# Tasks: rss-crawler
|
||||
|
||||
## 1. Схема и конфиг
|
||||
|
||||
- [x] 1.1 db/schema.sql: + CREATE TABLE rss_state; sources + feed_url; db/db.py — миграция ALTER TABLE (feed_url); sources.py — синк feed_url
|
||||
Проверка: `python -m db.db` (init_db) выполняется без ошибок; таблица rss_state и колонка sources.feed_url появляются
|
||||
- [x] 1.2 config.py: + RSS_USER_AGENT (env, default "vesti-rss/0.1"), RSS_DEFAULT_TIMEOUT=20
|
||||
Проверка: `python -c "from config import RSS_USER_AGENT; print(RSS_USER_AGENT)"` → значение из .env
|
||||
- [x] 1.3 requirements.txt: + trafilatura, selectolax
|
||||
Проверка: `.venv/bin/pip install -r requirements.txt` без ошибок
|
||||
|
||||
## 2. Модуль краулера
|
||||
|
||||
- [x] 2.1 crawler/rss_crawler.py: normalize_url (UTM/якоря), sha256_text
|
||||
Проверка: изолированный тест normalize_url: utm-параметры удаляются, якорь удаляется, остальное сохраняется
|
||||
- [x] 2.2 fetch_feed: httpx.GET с If-None-Match/If-Modified-Since/User-Agent, таймаут, ретрай 429/503; 304 → пусто
|
||||
Проверка: повторный запрос к ленте с etag → 304 (сервер LWN поддерживает)
|
||||
- [x] 2.3 parse_entries: feedparser, извлечение etag/modified из ответа, лимит записей (MAX_ITEMS=25)
|
||||
Проверка: запись → dict {url, title, text, published_at, author}
|
||||
- [x] 2.4 full_text: trafilatura для summary-only (selectolax для HTML→text)
|
||||
Проверка: запись с summary → в text полный текст или summary (не падает)
|
||||
- [x] 2.5 store_rss_post: дедуп по sha256; вставка в posts (source_id, url, canonical_url, content_type='text', published_at)
|
||||
Проверка: две идентичные записи → одна строка в posts
|
||||
- [x] 2.6 run_source: start_run/finish_run, обновление rss_state (etag/modified/status/error_count), status alive/dead при error_count>=5
|
||||
Проверка: успешный прогон → rss_state.status='alive', runs=ok; имитация ошибки → error_count растёт
|
||||
- [x] 2.7 CLI: --all / --direction / --source / --dry-run
|
||||
Проверка: `--source lwn --dry-run` печатает записи, БД не меняется
|
||||
|
||||
## 3. Источники и интеграция
|
||||
|
||||
- [x] 3.1 sources.yaml: + lwn (crawler: rss, direction: linux, priority: P0, enabled: true); пример opennet закомментирован
|
||||
Проверка: `sync_sources_to_db()` — в БД появляется источник lwn c crawler='rss'
|
||||
- [x] 3.2 .env.example: + RSS_USER_AGENT, RSS_DEFAULT_TIMEOUT
|
||||
- [x] 3.3 Реальный прогон `--source lwn` → посты в posts, rss_state настроена
|
||||
Проверка: SQL-запросы из design.md (Verification)
|
||||
|
||||
## 4. Cron и доки
|
||||
|
||||
- [ ] 4.1 Hermes cron: vesti-rss-crawler (`*/15`, P0; no_agent, тихий, deliver=local)
|
||||
Проверка: cron list показывает job; ручной запуск проходит
|
||||
- [x] 4.2 STATUS.md / AGENT.MD обновлены (команды запуска, порты/доступы)
|
||||
Проверка: grep rss в доках
|
||||
- [x] 4.3 `openspec validate rss-crawler` → 0 ошибок
|
||||
Проверка: команда выше → valid
|
||||
- [ ] 4.4 TODO.md: + задача веб-скедулинга RSS (веб-админ: кнопка запуска краулера)
|
||||
Проверка: задача видна в планах
|
||||
|
||||
Открытый вопрос: состояние таблицы rss_state ERROR (после 429 и ретрая, error_count=1) обновится при следующем удачном 200; в отсутствие ошибок не трогаем.
|
||||
Reference in New Issue
Block a user