# VESTI — Статус Обновлено: 2026-09-13 (publisher снова на Docker; systemd1 ожил) ## Текущее состояние - Базовый конвейер (прототип) работает: краулер (8 источников, инкрементальный, метрики), классификатор (Ollama qwen3:8b-nothink + словари), банк статей (bundles + origin:own), веб :8400 (кандидаты/approve/reject/опубликованные/метрики). - **Бэкфилл @dedinit ЗАВЕРШЁН** (2026-09-10): crawler/backfill_dedinit.py, fetched=1040, max_post_id=1092; в БД 846 постов is_own=1, медиа в media/media/ (исторический баг пути: БД media/, файл media/media/; с 2026-09-12 краулер качает в media/, для старых — fallback в make_card). Скрипт оставлен для повторного дозаполнения. - **РЕАЛЬНАЯ ПУБЛИКАЦИЯ РАБОТАЕТ**: бот-контроллер @dedinit_controller_bot (id 7765665742) → @dedinit_vesti. Токен в .env. Проверено: тестовые посты (message_id 2–4). - **publisher-service**: **Docker-контейнер** `vesti-publisher` (docker compose -f services/publisher/docker-compose.yml, 127.0.0.1:8410, restart=unless-stopped, healthy). Compose: env из /.env, TG_PROXY=socks5://host.docker.internal:1080, маунт ../../media/media:/srv/publisher/media:ro. Старый user-юнит `vesti-publisher.service` (~/.config/systemd/user/) — **disabled, не используется** (костыль на время сломанного systemd1, порт 8410 конфликтовал бы). - **Веб :8400**: systemd-юнит `/etc/systemd/system/vesti-web.service` (**enabled**, автостарт), uvicorn web.app:app на 0.0.0.0:8400, пароль из .env (ADMIN_PASSWORD). Доступен снаружи: https://vesti.nixg.ru. - **Cron (Hermes, no_agent, тихие)**: vesti-crawler-all-sources (`*/30`), vesti-watchdog (`*/15`, Telegram при проблемах). - **Бэкап**: `/opt/vesti/backup.sh` (по образцу icq/netbox) → `backups/vesti_<дата>.tar.gz` + Яндекс.Диск `/mnt/yandex-disk/backup/vesti-backups/`. Root cron `45 2 * * *` (лог /var/log/vesti-backup.log). Локально 7 дней, на ЯД 30 дней. ## Архитектура (реальная) ``` vesti-web (:8400) ──HTTP POST──▶ publisher-service (:8410) ──Bot API (SOCKS5 127.0.0.1:1080)──▶ Telegram approve (человек) VESTI_BOT_TOKEN, VESTI_BOT_CHANNELS @dedinit_vesti (+ другие) ``` - Бот-контроллер @dedinit_controller_bot публикует во все каналы, в которые добавлен администратором. - Каналы из конфига: VESTI_BOT_CHANNELS (сейчас @dedinit_vesti, потом несколько). - publisher-service: POST /api/v1/publish (card {text, media?, direction, lang}, channels?), GET /healthz, GET /api/v1/views/{channel}/{mid}. ## Сделано (сессия web-ui 2026-09-11) - [x] **Внешний доступ через Caddy**: https://vesti.nixg.ru (VPS02, /opt/caddy/Caddyfile, reverse_proxy 10.8.0.2:8400); юнит веба переведён на 0.0.0.0:8400; LE-серт выпущен - [x] **4 OpenSpec change** (по одному на замечание пользователя), все валидны (`openspec validate`): - `deexternalize-web-assets` — удалены unpkg (htmx 1.9.12) и jsdelivr (bootstrap 5.3.3); bootstrap локализован в web/static/bootstrap.min.css; approve/reject → обычные POST-формы. 0 внешних доменов в шаблонах. - `web-render-markdown` — Python-Markdown (>=3.6) + Jinja2-фильтр `markdown` (XSS-safe: escape→nl2br→sane_lists); тексты кандидатов/опубликованных рендерятся - `web-format-datetime` — фильтр `dt` → `ЧЧ:ММ ДД.ММ.ГГГГ` (candidates/published/metrics), ISO `[:16]` убран - `web-list-filter-no-js` — фильтрация чистыми GET-формами без JS/CDN (кнопка «Применить»); ожидания unpkg нет - [x] **Баг бандлов исправлен**: `create_bundle` возвращал абсолютный путь → в published.bundle_path лежал `/opt/vesti/bundles/...` → ссылки `/bundle//opt/...` (двойной слэш). Теперь относительный путь от bundles/ (`linux/2026-09/slug.md`), 4 строки БД обновлены, роут `/bundle/{path}` работает (HTTP 200) - [x] **AGENT.MD** — правила проекта (все изменения через OpenSpec и т.д.) ## Сделано (сессия publisher-service) - [x] OpenSpec change `publisher-service` — создан и ВАЛИДЕН (proposal, design, spec tg-publisher-service, tasks); `openspec validate` чисто - [x] services/publisher/ — FastAPI-микросервис: app/{main,config,telegram,channels}.py, requirements.txt, Dockerfile, docker-compose.yml, .env.example - [x] httpx[socks] установлен в venv (для SOCKS5-прокси к Bot API) - [x] Токен записан в .env (VESTI_BOT_TOKEN), чтение починил (load_dotenv override + путь BASE) - [x] Сервис запущен (venv, 127.0.0.1:8410); healthz: bot=dedinit_controller_bot, proxy=socks5://127.0.0.1:1080, channels=[@dedinit_vesti] - [x] **Реальная публикация**: POST /api/v1/publish → ok, message_id=2 в @dedinit_vesti (проверено) - [x] web/app.py: approve → HTTP-клиент web/publisher_client.py (publish + views), импорт publisher.bot убран - [x] .env.example обновлён (VESTI_BOT_CHANNELS, TG_PROXY, PUBLISHER_URL) - [x] **Docker publisher**: compose (пути ../../, extra_hosts host.docker.internal, TG_PROXY override, load_dotenv override=False), контейнер Up (healthy), healthz: bot/proxy/channels — ок (2026-09-09) - [x] **Реальный approve через веб** (сквозной: веб → HTTP → publisher(Docker) → Telegram): посты 136, 137 → @dedinit_vesti, tg_message_id 3 и 4, бандлы и статусы записаны - [x] **Веб-баги**: 500 на /candidates (raise RedirectResponse → HTTPException 303) и tg_message_id=0 для внешних (ключ результата — канал, не направление) — исправлены ## Сделано (сессия rich-repost-card 2026-09-12) - [x] **OpenSpec change `rich-repost-card`** — создан и ВАЛИДЕН (`openspec validate` чисто): proposal/design/tasks/specs/tg-publisher - [x] **Формат публикации — «богатый репост»** (publisher/card.py): карточка = [комментарий модератора] + полный текст поста + ссылка на оригинал. Служебка (📁📰👁) убрана. Текст plain (не HTML) — сырые <,>,& не искажаются - [x] **Предпросмотр ссылки отключён** (services/publisher/app/telegram.py): sendMessage → link_preview_options={is_disabled:true}; канал больше не выглядит «агрегатором ссылок» - [x] **Медиа правится**: main.py — если media_path существует: медиа первым (caption = первые 1000 симв., лимит 1024), полный текст отдельным сообщением; без медиа — один текст - [x] **Вебе добавлено поле «Комментарий»** (web/app.py + candidates.html): при approve комментарий встаёт первым блоком в публикацию - [x] **Фикс бага медиа-пути краулера** (crawler/telegram_crawler.py + backfill_dedinit.py): файл скачивался в media/media/, а media_path в БД = media/ → publisher не находил файл, медиа молча не публиковалось. Теперь скачивание в MEDIA_DIR/ (= media/). Для старых постов — fallback в make_card (пробует media/ и media/media/) - [x] telegram.py: добавлен delete_message (для отмены/чистки тестовых) - [x] **Проверено на реальном канале**: publish ушёл в @dedinit_vesti (message_id 6), текст с & доставлен как есть, без превью; тестовые сообщения удалены (delete_message ok). Канал чист - [x] **publisher временно работает как локальный uvicorn** (127.0.0.1:8410, PID 807769) — Docker недоступен из-за зависшего systemd1 на хосте (внешняя проблема, не vesti) ## Сделано ранее (own-content-hub) - [x] Миграция БД (migrate_own.py): is_own, is_own_canonical, sources.own, published.distributed_dirs - [x] sources.yaml: +dedinit (own: true), синк - [x] Краулер own-семантика; классификатор critical + мультинаправления; карточка с атрибуцией; веб «Свои»/бейдж/чекбоксы; бандлы origin:own - [x] Тест e2e (dry-run): approve с dirs [linux,ai] → /published с distributed_dirs ## В работе / Следующие шаги - [x] Бэкфилл своего канала @dedinit (задача 2.3) — **ЗАВЕРШЁН** (2026-09-10): fetched=1040, max_post_id=1092, 846 постов is_own=1 в БД - [x] publisher: медиа при Docker-запуске — **исправлено**: compose маунтит ../../media/media:/srv/publisher/media:ro - [x] systemd vesti-web.service + cron per-source + тихий watchdog — **юнит установлен** (подхватится при перезагрузке); **cron**: vesti-crawler-all-sources (`*/30`, тихий) + vesti-watchdog (`*/15`, Telegram при проблемах) - [x] Классификатор по неклассифицированным — **запущен** (2026-09-10, 870 постов, фон; статус в /tmp/vesti-classify.log) - [ ] Проверить результат классификации (в БД: direction заполнены) и выборочно качество на своих постах - [ ] **rich-repost-card в проде**: после оживления systemd1 на хосте пересобрать Docker-контейнер publisher (docker compose -f services/publisher/docker-compose.yml up -d --build), перезапустить веб (подхватит шаблон + comment), проверить approve поста с медиа (send_photo/send_video) - [ ] **Закрыть OpenSpec change rich-repost-card** (статус APPLIED после подтверждения), архивировать - [ ] git-репозитории (gitverse истина, gitea зеркало) — /opt/vesti ещё НЕ git-репо - [ ] openspec archive web-изменений (deexternalize-web-assets, web-render-markdown, web-format-datetime, web-list-filter-no-js) и publisher-service / tg-crawler-publisher-prototype (после подтверждения) ## Как запустить / проверить ```bash cd /opt/vesti # publisher-service (ОСНОВНОЙ способ — Docker): docker compose -f services/publisher/docker-compose.yml up -d --build # контейнер vesti-publisher, :8410 curl -s http://127.0.0.1:8410/healthz # ok, bot=dedinit_controller_bot, proxy=socks5://host.docker.internal:1080, channels docker logs -f vesti-publisher # логи curl -s -X POST http://127.0.0.1:8410/api/v1/publish \ -H 'Content-Type: application/json' \ -d '{"card":{"text":"Тест","direction":"linux","lang":"ru"}}' # (старый user-юнит vesti-publisher.service disabled — не использовать, порт занят) # веб (пароль из .env: VESTI_WEB_PASSWORD or ADMIN_PASSWORD): # systemd-юнит: /etc/systemd/system/vesti-web.service (после daemon-reload) # вручную: set -a; . ./.env; set +a; .venv/bin/uvicorn web.app:app --host 127.0.0.1 --port 8400 # краулер/классификатор: .venv/bin/python -m crawler.telegram_crawler --all # инкрементально все источники .venv/bin/python -m crawler.backfill_dedinit # бэкфилл своего канала (разовый) CLASSIFY_TIMEOUT=20 .venv/bin/python -m classifier.classify --db db/vesti.db --limit 200 ``` ## Ключевые артефакты - /opt/vesti/AGENT.MD — правила проекта (обязательно: все изменения через OpenSpec) - /opt/vesti/openspec/changes/{deexternalize-web-assets, web-render-markdown, web-format-datetime, web-list-filter-no-js}/ — 4 change веб-интерфейса (валидны, задачи [x]) - /opt/vesti/openspec/changes/rich-repost-card/ — change формата публикации (proposal/design/tasks/specs/tg-publisher, валиден) - /opt/vesti/services/publisher/ — микросервис (app/, Dockerfile, compose, README) - /opt/vesti/web/publisher_client.py — HTTP-клиент веба к микросервису - /opt/vesti/openspec/changes/publisher-service/ — OpenSpec change (proposal/design/spec/tasks) - publisher/bot.py — старое (логика перенесена в services/publisher/app/telegram.py) - publisher/card.py — карточка (осталась) - db/vesti.db, sources/sources.yaml, crawler/, classifier/, web/, bundles/ ## Открытые вопросы - ~~Медиа при Docker-запуске~~ — решено: compose маунтит ../../media/media:/srv/publisher/media (веб шлёт media/) - **publisher в проде = Docker** (2026-09-13): systemd1 (D-Bus org.freedesktop.systemd1) после перезагрузки ожил, контейнер vesti-publisher (compose, restart=unless-stopped) работает healthy. User-юнит vesti-publisher.service выключен (disabled) и не используется. Если снова сломается systemd1 — вернуть юнит: systemctl --user enable --now vesti-publisher.service. - Fan-out: сейчас все каналы из VESTI_BOT_CHANNELS (один). В будущем — направления→каналы (маппинг). - ~~Бэкфилл канала (задача 2.3)~~ — ЗАВЕРШЁН (2026-09-10): 1040 постов, 846 в БД is_own=1 - git-репозиторий /opt/vesti — инициализировать? - systemd daemon-reload на bigbox зависает (внешняя проблема, не vesti) — юнит подхватится при перезагрузке - Качество классификации своих постов (846, словари vs LLM) — проверить выборочно