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,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.
|
||||
- **Право на медиа**: медиа своего канала скачивается (контент пользователя) — ок.
|
||||
Reference in New Issue
Block a user