mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
Initial import: vesti.nixg.ru — новостной апрув-проект (web, crawler, classifier, publisher, openspec)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-08
|
||||
@@ -0,0 +1,106 @@
|
||||
# Design: own-content-hub
|
||||
|
||||
## Approach
|
||||
|
||||
Канал @dedinit — обычный источник в реестре, но с флагом `own: true`. Вся общая логика
|
||||
(краулер, дедуп, классификатор, банк) работает как для любого TG-канала; различие —
|
||||
семантика: посты своего канала считаются СВОИМ контентом (is_own=1) и становятся
|
||||
«сильными кандидатами» на автораспространение по всем тематическим лентам.
|
||||
|
||||
Ключевая идея — **fan-out вместо single-out**: один пост пользователя при подтверждении
|
||||
уходит сразу во все тематические каналы @dedinit_vesti_<direction>_<lang>_bot, под
|
||||
которые он подходит (направления из классификации). Так контент «инъецируется» в
|
||||
новостные ленты и собирает аудиторию на всех площадках, а сам канал-источник остаётся
|
||||
первоисточником (атрибуция везде).
|
||||
|
||||
Поток данных:
|
||||
```
|
||||
sources.yaml: @dedinit (own: true)
|
||||
│
|
||||
▼
|
||||
telegram_crawler.py → posts.is_own=1 (дедуп как обычно; медиа скачивается — свой контент)
|
||||
▼
|
||||
classifier.py → направление(я) + relevance; is_own + критичность → «сильный кандидат»
|
||||
▼
|
||||
vesti-web (фильтр «Свои», бейдж; подтверждение с выбором направлений рассылки)
|
||||
▼
|
||||
tg-publisher.py → fan-out: карточка в каждый @dedinit_vesti_<dir>_<lang>_bot
|
||||
▼
|
||||
news-store → бандл bundles/<dir>/<YYYY-MM>/<slug>.md (origin=own, ссылка на оригинал)
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
```bash
|
||||
# Изменяемые файлы
|
||||
sources/sources.yaml # + источник dedinit (own: true)
|
||||
db/schema.sql # + posts.is_own, posts.is_own_canonical, sources.own,
|
||||
# published.distributed_dirs (миграция ALTER TABLE)
|
||||
crawler/telegram_crawler.py # + определение own-источника, проставление is_own,
|
||||
# is_own_canonical (первый экземпляр = канал), медиа скачивается
|
||||
classifier/classify.py # + is_own → «сильный кандидат» (relevance critical, classified=True)
|
||||
publisher/bot.py # + fan-out publish_multi(directions)
|
||||
publisher/card.py # + атрибуция «Дед в АйТи» + ссылка на оригинал
|
||||
web/app.py # + фильтр is_own, бейдж, выбор направлений рассылки при approve
|
||||
web/templates/candidates.html # + бейдж СВОЙ, чекбоксы направлений
|
||||
web/store.py # + frontmatter origin: own + ссылка на оригинал
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# 1) Миграция схемы (идиемпотентно)
|
||||
cd /opt/vesti && .venv/bin/python - <<'PY'
|
||||
import sqlite3
|
||||
c = sqlite3.connect('db/vesti.db')
|
||||
for ddl in [
|
||||
"ALTER TABLE posts ADD COLUMN is_own INTEGER DEFAULT 0",
|
||||
"ALTER TABLE posts ADD COLUMN is_own_canonical INTEGER DEFAULT 0",
|
||||
"ALTER TABLE sources ADD COLUMN own INTEGER DEFAULT 0",
|
||||
"ALTER TABLE published ADD COLUMN distributed_dirs TEXT",
|
||||
]:
|
||||
try: c.execute(ddl)
|
||||
except sqlite3.OperationalError: pass # уже есть
|
||||
c.commit(); c.close()
|
||||
PY
|
||||
|
||||
# 2) Добавить источник в sources.yaml (own: true), синк
|
||||
.venv/bin/python -c "from sources.sources import sync_sources_to_db, load_sources_yaml; sync_sources_to_db(load_sources_yaml())"
|
||||
|
||||
# 3) Краулер по своему каналу (бэкфилл ~1039 постов; медиа скачивается)
|
||||
.venv/bin/python -m crawler.telegram_crawler --source dedinit
|
||||
|
||||
# 4) Классификатор по своему каналу
|
||||
CLASSIFY_TIMEOUT=20 .venv/bin/python -m classifier.classify --direction linux # + нужные направления
|
||||
|
||||
# 5) Веб
|
||||
VESTI_WEB_PASSWORD=<пароль> .venv/bin/uvicorn web.app:app --host 127.0.0.1 --port 8400
|
||||
|
||||
# 6) Проверка
|
||||
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8400/login # 200
|
||||
sqlite3 db/vesti.db "SELECT COUNT(*) FROM posts WHERE is_own=1" # >0
|
||||
sqlite3 db/vesti.db "SELECT COUNT(*) FROM posts WHERE is_own=1 AND is_own_canonical=1"
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
# Отключить источник (не удалять данные)
|
||||
# sources.yaml: dedinit → enabled: false, own: false
|
||||
# Перезапустить синк; посты остаются в БД, новые не приходят.
|
||||
# Поля is_own в данных можно оставить (безвредно); удаление данных — по согласованию.
|
||||
```
|
||||
|
||||
Существующие внешние источники и их посты не затрагиваются: is_own=0 по умолчанию.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Бэкфилл 1039 постов** — первый прогон долгий (медиа ~623M). Ограничить
|
||||
MAX_POSTS_PER_CHANNEL или сначала только текст, медиа докачать позже (бэкфилл-флаг).
|
||||
- **Дубликаты с внешними источниками**: пост пользователя, который запостили в чужой
|
||||
канал, попадёт и как is_own (свой), и как внешний. Для своих постов `is_own_canonical=1`
|
||||
(первичный экземпляр), внешние остаются как «упоминания» (is_own=0).
|
||||
- **Fan-out = спам**: всегда режим подтверждения; веб показывает направления заранее;
|
||||
лимит 4096 символов сохраняется; атрибуция не даёт путаницы с чужим контентом.
|
||||
- **qwen3:8b think:false** — уже учтено в classify.py.
|
||||
- **Право на медиа**: медиа своего канала скачивается (контент пользователя) — ок.
|
||||
@@ -0,0 +1,65 @@
|
||||
## Why
|
||||
|
||||
У пользователя есть собственные ресурсы (канал Telegram «Дед в АйТи» @dedinit, сайт dedinit.ru,
|
||||
далее — феды/видео), но они живут разрозненно: контент, опубликованный в одном месте, не
|
||||
попадает в другие. Цель — **собственный контент-хаб**: единая точка сбора ВСЕХ постов
|
||||
пользователя из разных платформ, один банк своего контента, и **автораспространение**
|
||||
этого контента по тематическим новостным ботам VESTI (и в перспективе — по другим
|
||||
платформам), чтобы органически росла аудитория на всех площадках.
|
||||
|
||||
Задача НЕ «сделать копию канала»: канал @dedinit — полноценный источник данных в общей
|
||||
логике проекта (как любой TG-канал): краулер → классификатор → кандидат → подтверждение →
|
||||
публикация в тематические каналы → банк статей. Отличие от внешних источников — это
|
||||
СВОЙ контент (is_own=1): он всегда кандидат на публикацию («инъекция» в ленту),
|
||||
не блокируется политикой чужих форвардов и помечается атрибуцией автора.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Добавить канал @dedinit (id 1150165846) в sources.yaml как источник `own: true`
|
||||
(свой контент пользователя), направление определяется классификатором (канал
|
||||
разносторонний: Linux, IT, AI, игры...).
|
||||
- Краулер: посты своего канала помечаются `is_own=1`, для них продолжает работать
|
||||
дедуп (sha256/url); политика форвардов для своего канала — как обычно (чужие
|
||||
форварды → fwd-поля без медиа).
|
||||
- Классификатор: свой контент с relevance critical/high → «сильные кандидаты»
|
||||
(самокатегоризация; приоритет в ленте подтверждения).
|
||||
- **Автораспространение (distribution)**: подтверждённый пост рассылается НЕ только
|
||||
в один канал @dedinit_vesti_<dir>_<lang>_bot, а во ВСЕ тематические боты/каналы,
|
||||
соответствующие направлениям поста (один пост может попасть в несколько лент).
|
||||
Режим — по-прежнему «черновик на подтверждение», но подтверждение ведёт к
|
||||
множественной публикации (fan-out).
|
||||
- Веб: фильтр «Свои» (только is_own посты), отображение бейджа «СВОЙ», выбор
|
||||
направлений для рассылки при подтверждении.
|
||||
- Расширить таблицы: posts.is_own, posts.is_own_canonical (первичный экземпляр),
|
||||
sources.own, published.distributed_dirs (какие направления розданы).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `own-content`: Свой контент-хаб: пометка постов пользователя (is_own), приоритет
|
||||
в кандидатах, сквозная ссылка на оригинал во всех публикациях.
|
||||
|
||||
### Modified Capabilities
|
||||
- `tg-crawler`: пометка is_own по источникам с own: true; политика форвардов для
|
||||
своих каналов не отличается от внешних (dedup + fwd-поля).
|
||||
- `classifier`: свой контент → «сильный кандидат» (relevance critical/high при
|
||||
словарном попадании; classified=True даже без LLM-подтверждения, если есть
|
||||
направление).
|
||||
- `tg-publisher`: fan-out по нескольким направлениям; атрибуция «Дед в АйТи» +
|
||||
ссылка на оригинал.
|
||||
- `vesti-web`: фильтр «Свои», бейдж, выбор направлений рассылки при подтверждении.
|
||||
- `news-store`: бандл своего поста помечается origin=own + ссылка на оригинал в
|
||||
frontmatter.
|
||||
|
||||
## Impact
|
||||
|
||||
- Затронутые сервисы/порты: без новых портов; краулер (cron), классификатор,
|
||||
tg-publisher, веб 127.0.0.1:8400 — те же.
|
||||
- Файлы: sources/sources.yaml (новый источник), crawler/telegram_crawler.py,
|
||||
classifier/classify.py, publisher/bot.py, publisher/card.py, web/app.py,
|
||||
db/schema.sql (миграции ADD COLUMN is_own и др.).
|
||||
- Данные: 1039 постов канала @dedinit появятся как is_own посты; медиа своего канала
|
||||
скачивается (это контент пользователя — можно).
|
||||
- Секреты: не требуются новые (та же Telethon-сессия, токены ботов в .env).
|
||||
- Rollback: отключение источника `own: false` / удаление поля is_own; существующие
|
||||
внешние посты не затрагиваются; данные не удаляются (правило пользователя).
|
||||
@@ -0,0 +1,30 @@
|
||||
## Purpose
|
||||
|
||||
Классификация постов по направлениям локальным LLM + словарный фильтр. Дополняется
|
||||
приоритетом «своего контента» и поддержкой мультинаправлений для fan-out.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Приоритет своего контента
|
||||
Классификатор MUST обрабатывать посты с `is_own=1` как «сильные кандидаты»: при наличии
|
||||
направления по словарю — relevance=critical, classified=True — даже если LLM недоступна
|
||||
(фолбэк без LLM, пост не теряется). Для внешних постов поведение без изменений.
|
||||
|
||||
#### Scenario: Свой пост, LLM недоступна
|
||||
- **WHEN** пост is_own=1, словарь дал направление, но Ollama недоступна
|
||||
- **THEN** пост получает direction (словарь), relevance=critical, classified=True,
|
||||
method='dict-own' (не требует LLM)
|
||||
|
||||
#### Scenario: Свой пост без направления по словарю
|
||||
- **WHEN** пост is_own=1, словарь не дал направление
|
||||
- **THEN** классификации нет (classified=False) до LLM; пост не теряется (остаётся в очереди)
|
||||
|
||||
### Requirement: Мультинаправления
|
||||
Классификатор MUST уметь возвращать несколько направлений для поста (для маппинга fan-out);
|
||||
основное направление хранится в posts.direction, дополнительные — в classifications
|
||||
(таблица уже позволяет несколько классификаций на пост).
|
||||
|
||||
#### Scenario: Пост про Linux + AI
|
||||
- **WHEN** пост упоминает и линукс, и нейросети
|
||||
- **THEN** в classifications может быть несколько записей (linux, ai); fan-out использует
|
||||
оба при подтверждении
|
||||
@@ -0,0 +1,21 @@
|
||||
## Purpose
|
||||
|
||||
Банк статей: markdown-бандлы с frontmatter + медиа. Дополняется пометкой происхождения
|
||||
своего контента и ссылкой на оригинал.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Атрибуция в бандле
|
||||
Бандл поста с is_own=1 MUST содержать в frontmatter `origin: own`, ссылку на оригинал
|
||||
(`source_url` = t.me/dedinit/<id>) и имя автора («Дед в АйТи»). Бандл внешнего поста —
|
||||
как раньше (origin: external, source_url=url источника).
|
||||
|
||||
#### Scenario: Бандл своего поста
|
||||
- **WHEN** create_bundle вызывается для поста is_own=1
|
||||
- **THEN** frontmatter содержит origin: own, source: dedinit, source_url:
|
||||
https://t.me/dedinit/<tg_post_id>, author: Дед в АйТи
|
||||
|
||||
#### Scenario: Бандл внешнего поста в нескольких направлениях
|
||||
- **WHEN** пост (свой или внешний) опубликован в несколько направлений
|
||||
- **THEN** бандл создаётся по каждому направлению (bundles/<dir>/<YYYY-MM>/<slug>.md),
|
||||
обе записи ссылаются на один и тот же original post_id
|
||||
@@ -0,0 +1,32 @@
|
||||
## Purpose
|
||||
|
||||
Чтение публичных Telegram-каналов через Telethon (MTProto) для сбора новостей с метриками
|
||||
популярности. Дополняется поддержкой «своих» источников (own: true) — контент пользователя.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Источники своего контента
|
||||
Краулер MUST распознавать источники с `own: true` в sources.yaml и для их постов
|
||||
проставлять `is_own=1`, `is_own_canonical=1`. Для таких источников медиа MUST
|
||||
скачиваться (контент принадлежит пользователю).
|
||||
|
||||
#### Scenario: Краулинг своего канала
|
||||
- **WHEN** источник slug=dedinit имеет own: true
|
||||
- **THEN** посты сохраняются с is_own=1 и is_own_canonical=1; медиа скачивается;
|
||||
инкрементальный обход и дедуп работают как обычно
|
||||
|
||||
#### Scenario: Чужой форвард в своём канале
|
||||
- **WHEN** пост в своём канале — форвард из чужого канала
|
||||
- **THEN** пост сохраняется с is_own=1 (это пост пользователя, он его переслал) и с
|
||||
fwd_from_channel_id/fwd_from_post_id; медиа не скачивается (содержимое чужое),
|
||||
атрибуция оригинала сохраняется в fwd-полях
|
||||
|
||||
### Requirement: Канонический экземпляр своего контента
|
||||
Если пост пользователя (is_own=1) позже встречается во внешнем канале (тот же sha256/url),
|
||||
внешний экземпляр MUST сохраняться как «упоминание» с is_own=0; каноническим остаётся
|
||||
первичный (is_own_canonical=1).
|
||||
|
||||
#### Scenario: Свой пост запостили в чужой канал
|
||||
- **WHEN** краулер находит в чужом канале пост с текстом, совпадающим с is_own-постом
|
||||
- **THEN** создаётся запись is_own=0 (упоминание) без дублирования контента; веб видит
|
||||
оба экземпляра, но кандидатом на публикацию считается канонический (is_own_canonical)
|
||||
@@ -0,0 +1,43 @@
|
||||
## Purpose
|
||||
|
||||
Публикация отобранных новостей через Bot API в тематические Telegram-каналы. Дополняется
|
||||
автораспространением своего контента (fan-out) по нескольким направлениям.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Автораспространение (fan-out)
|
||||
Подтверждение СВОЕГО поста (is_own=1) MUST публиковать карточку во ВСЕ тематические
|
||||
каналы @dedinit_vesti_<direction>_<lang>_bot, соответствующие выбранным направлениям
|
||||
(по умолчанию — все направления классификации поста). Каждая карточка MUST содержать
|
||||
атрибуцию «Дед в АйТи» (@dedinit) и ссылку на оригинал t.me/dedinit/<post_id>.
|
||||
|
||||
#### Scenario: Мульти-публикация
|
||||
- **WHEN** подтверждается свой пост с направлениями [linux, ai]
|
||||
- **THEN** карточка отправляется в @dedinit_vesti_linux_ru_bot и @dedinit_vesti_ai_ru_bot;
|
||||
обе содержат ссылку на оригинал
|
||||
|
||||
#### Scenario: Чужой пост — без fan-out
|
||||
- **WHEN** подтверждается внешний пост (is_own=0)
|
||||
- **THEN** публикуется только в канал своего направления, без атрибуции автора
|
||||
|
||||
### Requirement: Публикация по списку направлений
|
||||
Публикатор MUST поддерживать `publish_multi(directions)` — публикацию карточки по списку
|
||||
направлений, возвращающую map {direction: message_id}. При пустом списке направлений
|
||||
MUST публиковать в направление по умолчанию (direction поста).
|
||||
|
||||
#### Scenario: Список направлений рассылки
|
||||
- **WHEN** publish_multi вызывается с directions=[linux, ai]
|
||||
- **THEN** возвращается {linux: message_id1, ai: message_id2}; каждая публикация
|
||||
записывается в published с distributed_dirs
|
||||
|
||||
#### Scenario: Пустой список направлений
|
||||
- **WHEN** publish_multi вызывается без directions
|
||||
- **THEN** публикация идёт в канал направления поста (direction по умолчанию), без fan-out
|
||||
|
||||
### Requirement: Метрики по каждому направлению
|
||||
Для опубликованных карточек MUST собираться views через Bot API по каждому направлению
|
||||
(каждому message_id), чтобы веб показывал эффективность рассылки по лентам.
|
||||
|
||||
#### Scenario: Метрики fan-out
|
||||
- **WHEN** пост разослан в [linux, ai] и каналы набирают просмотры
|
||||
- **THEN** get_views вызывается для каждого message_id; views хранятся по направлению
|
||||
@@ -0,0 +1,28 @@
|
||||
## Purpose
|
||||
|
||||
Веб-интерфейс управления VESTI. Дополняется фильтром «Свои», бейджем и выбором
|
||||
направлений рассылки для своего контента.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Фильтр «Свои»
|
||||
Веб MUST давать фильтр постов по `is_own` (все/только свои/только внешние) и показывать
|
||||
бейдж «СВОЙ» у постов is_own=1. Для своего поста при подтверждении MUST отображаться
|
||||
выбор направлений рассылки (по умолчанию — все направления классификации поста).
|
||||
|
||||
#### Scenario: Фильтр своих постов
|
||||
- **WHEN** админ выбирает фильтр «Свои»
|
||||
- **THEN** показываются только посты is_own=1 с бейджем «СВОЙ» и чекбоксами направлений
|
||||
|
||||
#### Scenario: Подтверждение своего поста
|
||||
- **WHEN** админ подтверждает свой пост с выбранными направлениями [linux, ai]
|
||||
- **THEN** публикация идёт в оба канала (fan-out), результат виден в опубликованных с
|
||||
distributed_dirs
|
||||
|
||||
### Requirement: Список распространения
|
||||
Веб MUST показывать для опубликованного поста, в какие направления/каналы он был
|
||||
разослан (distributed_dirs) и метрики (views) по каждому каналу.
|
||||
|
||||
#### Scenario: Просмотр распространения
|
||||
- **WHEN** админ открывает опубликованный пост (свой)
|
||||
- **THEN** видит список @dedinit_vesti_<dir>_<lang>_bot с views по каждому
|
||||
@@ -0,0 +1,60 @@
|
||||
# Tasks: own-content-hub
|
||||
|
||||
## 1. Схема БД и реестр источников
|
||||
|
||||
- [x] 1.1 Миграция схемы: ALTER TABLE posts ADD COLUMN is_own INTEGER DEFAULT 0, is_own_canonical INTEGER DEFAULT 0; sources ADD COLUMN own INTEGER DEFAULT 0; published ADD COLUMN distributed_dirs TEXT
|
||||
Проверка: `sqlite3 db/vesti.db "PRAGMA table_info(posts)"` показывает is_own/is_own_canonical; sources.own; published.distributed_dirs
|
||||
- [x] 1.2 Добавить источник dedinit в sources.yaml (channel: dedinit, own: true, direction: null, lang: ru)
|
||||
Проверка: `grep -A4 "slug: dedinit" sources/sources.yaml` → есть own: true
|
||||
- [x] 1.3 Синк реестра в БД (sources.own=1 для dedinit)
|
||||
Проверка: `sqlite3 db/vesti.db "SELECT slug, own FROM sources WHERE slug='dedinit'"` → dedinit|1
|
||||
|
||||
## 2. Краулер
|
||||
|
||||
- [x] 2.1 telegram_crawler.py: загрузка own-флага источников; проставление is_own/is_own_canonical для own-источников; медиа скачивается
|
||||
Проверка: код реализован (store_posts/is_own_source, py_compile OK); интеграционная проверка ждёт бэкфилла 2.3
|
||||
- [x] 2.2 Форварды в своём канале: чужой форвард сохраняется с is_own=1 + fwd-полями, медиа НЕ скачивается
|
||||
Проверка: код реализован (skip_media для форвардов); интеграционная проверка после бэкфилла
|
||||
- [ ] 2.3 Бэкфилл своего канала: первый прогон ~1039 постов (медиа по возможности; при лимите — текст без медиа, бэкфилл-флаг)
|
||||
Проверка: `sqlite3 db/vesti.db "SELECT COUNT(*) FROM posts WHERE is_own=1"` > 0 (до ~1039)
|
||||
- [x] 2.4 Дедуп: тот же sha256/url во внешнем канале → is_own=0 (упоминание), канонический остаётся is_own_canonical=1
|
||||
Проверка: код реализован (канон vs упоминание); проверка на реальных данных после бэкфилла
|
||||
|
||||
## 3. Классификатор
|
||||
|
||||
- [x] 3.1 classify.py: is_own=1 + направление по словарю → relevance=critical, classified=True, method='dict-own' (без LLM при недоступности)
|
||||
Проверка: тестовый свой пост со словарным попаданием при выключенной Ollama → classified=1 relevance=critical
|
||||
- [x] 3.2 Поддержка мультинаправлений: классификатор может писать несколько записей classifications (fan-out)
|
||||
Проверка: пост linux+ai имеет 2 записи classifications
|
||||
|
||||
## 4. Публикатор (fan-out)
|
||||
|
||||
- [x] 4.1 publisher/bot.py: publish_multi(directions) → публикация карточки в каждый @dedinit_vesti_<dir>_<lang>_bot; возвращает {dir: message_id}
|
||||
Проверка: dry-run с токеном → map направлений; без токена → dry_run=True (проверено ['linux','ai'])
|
||||
- [x] 4.2 Атрибуция в карточке (publisher/card.py): для is_own-постов строка «Дед в АйТи (@dedinit)» + ссылка на оригинал, для внешних — без
|
||||
Проверка: make_card(is_own пост) содержит t.me/dedinit/ и «Дед в АйТи» (проверено); make_card(внешний) — нет
|
||||
- [x] 4.3 Запись distributed_dirs + tg_message_ids в published при fan-out
|
||||
Проверка: после approve (TestClient, dry-run) distributed_dirs=JSON([linux, ai]) в published + views
|
||||
|
||||
## 5. Веб
|
||||
|
||||
- [x] 5.1 Фильтр «Свои» (is_own) в /candidates: параметр own=1|0, бейдж «СВОЙ»
|
||||
Проверка: TestClient GET /candidates?own=1 → только is_own посты с бейджем «⭐ СВОЙ» (проверено)
|
||||
- [x] 5.2 Выбор направлений рассылки при approve: чекбоксы (по умолчанию — направления классификации); approve → publish_multi
|
||||
Проверка: TestClient POST /posts/{id}/approve с dirs=linux,ai → 302 /published, distributed_dirs=[linux,ai] (dry-run) (проверено)
|
||||
- [x] 5.3 Опубликованные: показ distributed_dirs (в какие каналы разослан) и метрики по каждому
|
||||
Проверка: /published содержит «Разослан в» с @dedinit_vesti_linux_ru_bot и ai (проверено TestClient)
|
||||
|
||||
## 6. Банк статей (news-store)
|
||||
|
||||
- [x] 6.1 store.py: frontmatter origin: own + source_url + author для is_own-постов; origin: external для внешних
|
||||
Проверка: бандл содержит `origin: own` и `source_url: https://t.me/dedinit/<id>`, author «Дед в АйТи» (проверено)
|
||||
- [x] 6.2 Бандлы по каждому направлению fan-out (bundles/<dir>/<YYYY-MM>/<slug>.md)
|
||||
Проверка: create_bundle(['linux','ai']) → 2 файла в bundles/linux и bundles/ai (проверено)
|
||||
|
||||
## 7. Проверка интеграции и документация
|
||||
|
||||
- [ ] 7.1 Полный прогон: синк → краулер dedinit → классификатор → веб (approve с fan-out) → бандлы; внешние источники не затронуты (is_own=0 по умолчанию)
|
||||
Проверка: counts по is_own в БД, бандлы, /published, runs ok — ждёт бэкфилла 2.3 (реальная сеть)
|
||||
- [x] 7.2 Обновить STATUS.md / TODO.md / WALKTHROUGH.md (что сделано, как запускать, питфолы)
|
||||
Проверка: документы отражают новое состояние (обновлено при закрытии сессии)
|
||||
Reference in New Issue
Block a user