Files
vesti/STATUS.md
T

34 KiB
Raw Blame History

VESTI — Статус

Обновлено: 2026-09-16 (address-field-enhancement заархивирован; активны: manual-direction, extract-post-sources)

Текущее состояние

  • address-field-enhancement — ЗААРХИВИРОВАН (2026-09-16). Сделано и в проде: форма добавления источника — одно поле Address. Для telegram: https://t.me/mknewsru → slug=mknewsru, название «Мой Компьютер» (Telethon get_entity); для rss: https://lwn.net/headlines/rss → slug=lwn, название «LWN.net» (feedparser). Кнопка «🔎 Найти» в форме. direction — datalist из реальных значений (DIRECTIONS + БД), свободный ввод остаётся. БД не менялась (address — только UI-концепция, в yaml по-прежнему url/channel/feed_url).
  • fix-sources-add-channel (2026-09-16): добавление telegram-источника через веб снова работает. Поле channel добавлено в форму (required для telegram через JS) и показывается в таблице. Причина: UI-форма не имела поля channel. web/templates/sources.html ранее отсутствовал в git — добавлен.
  • FEDIVERSE-ПУБЛИКАЦИЯ РАБОТАЕТ (2026-09-13): publisher-service умеет публиковать карточки в GoToSocial (social.dedinit.ru) от имени бот-аккаунта @vesti@dedinit.ru (Mastodon-совместимый API). Change gotosocial-publisher.
  • Аккаунт vesti создан на GtS (2026-09-13, confirmed+approved, admin=0), OAuth-приложение vesti-publisher (client_id=01B09QNDM9YQ2VQVSRXHCVAV6K), токен в .env (GT_SOCIAL_ACCESS_TOKEN, scope read write), канал VESTI_GTS_CHANNELS=@vesti@dedinit.ru.
  • healthz publisher: блок gotosocial — url, account=vesti, token_set=true, account_ok=true.
  • Реальные тесты: текст → пост, медиа → пост с картинкой (media_attachments=1), delete → 200. Посты-тесты удалены, канал чист.
  • Канал в запросе: channels: ["gt:@vesti@dedinit.ru"] (префикс gt:) — fediverse; @dedinit_vesti — Telegram. dry_run эмулирует оба.
  • Базовый конвейер (прототип) работает (далее по-прежнему): краулер, классификатор, банк статей, веб :8400.
  • Бэкфилл @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)

  • Внешний доступ через Caddy: https://vesti.nixg.ru (VPS02, /opt/caddy/Caddyfile, reverse_proxy 10.8.0.2:8400); юнит веба переведён на 0.0.0.0:8400; LE-серт выпущен
  • 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 нет
  • Баг бандлов исправлен: create_bundle возвращал абсолютный путь → в published.bundle_path лежал /opt/vesti/bundles/... → ссылки /bundle//opt/... (двойной слэш). Теперь относительный путь от bundles/ (linux/2026-09/slug.md), 4 строки БД обновлены, роут /bundle/{path} работает (HTTP 200)
  • AGENT.MD — правила проекта (все изменения через OpenSpec и т.д.)

Сделано (сессия publisher-service)

  • OpenSpec change publisher-service — создан и ВАЛИДЕН (proposal, design, spec tg-publisher-service, tasks); openspec validate чисто
  • services/publisher/ — FastAPI-микросервис: app/{main,config,telegram,channels}.py, requirements.txt, Dockerfile, docker-compose.yml, .env.example
  • httpx[socks] установлен в venv (для SOCKS5-прокси к Bot API)
  • Токен записан в .env (VESTI_BOT_TOKEN), чтение починил (load_dotenv override + путь BASE)
  • Сервис запущен (venv, 127.0.0.1:8410); healthz: bot=dedinit_controller_bot, proxy=socks5://127.0.0.1:1080, channels=[@dedinit_vesti]
  • Реальная публикация: POST /api/v1/publish → ok, message_id=2 в @dedinit_vesti (проверено)
  • web/app.py: approve → HTTP-клиент web/publisher_client.py (publish + views), импорт publisher.bot убран
  • .env.example обновлён (VESTI_BOT_CHANNELS, TG_PROXY, PUBLISHER_URL)
  • Docker publisher: compose (пути ../../, extra_hosts host.docker.internal, TG_PROXY override, load_dotenv override=False), контейнер Up (healthy), healthz: bot/proxy/channels — ок (2026-09-09)
  • Реальный approve через веб (сквозной: веб → HTTP → publisher(Docker) → Telegram): посты 136, 137 → @dedinit_vesti, tg_message_id 3 и 4, бандлы и статусы записаны
  • Веб-баги: 500 на /candidates (raise RedirectResponse → HTTPException 303) и tg_message_id=0 для внешних (ключ результата — канал, не направление) — исправлены

Сделано (сессия rich-repost-card 2026-09-12)

  • OpenSpec change rich-repost-card — создан и ВАЛИДЕН (openspec validate чисто): proposal/design/tasks/specs/tg-publisher
  • Формат публикации — «богатый репост» (publisher/card.py): карточка = [комментарий модератора] + полный текст поста + ссылка на оригинал. Служебка (📁📰👁) убрана. Текст plain (не HTML) — сырые <,>,& не искажаются
  • Предпросмотр ссылки отключён (services/publisher/app/telegram.py): sendMessage → link_preview_options={is_disabled:true}; канал больше не выглядит «агрегатором ссылок»
  • Медиа правится: main.py — если media_path существует: медиа первым (caption = первые 1000 симв., лимит 1024), полный текст отдельным сообщением; без медиа — один текст
  • Вебе добавлено поле «Комментарий» (web/app.py + candidates.html): при approve комментарий встаёт первым блоком в публикацию
  • Фикс бага медиа-пути краулера (crawler/telegram_crawler.py + backfill_dedinit.py): файл скачивался в media/media/, а media_path в БД = media/ → publisher не находил файл, медиа молча не публиковалось. Теперь скачивание в MEDIA_DIR/ (= media/). Для старых постов — fallback в make_card (пробует media/ и media/media/)
  • telegram.py: добавлен delete_message (для отмены/чистки тестовых)
  • Проверено на реальном канале: publish ушёл в @dedinit_vesti (message_id 6), текст с & доставлен как есть, без превью; тестовые сообщения удалены (delete_message ok). Канал чист
  • publisher временно работает как локальный uvicorn (127.0.0.1:8410, PID 807769) — Docker недоступен из-за зависшего systemd1 на хосте (внешняя проблема, не vesti)

Сделано (сессия gotosocial-publisher 2026-09-13)

  • OpenSpec change gotosocial-publisher — proposal/design/tasks/specs/gotosocial-publisher, openspec validate чисто
  • services/publisher/app/gotosocial.py — клиент GtS (httpx): post_status (format=markdown), upload_media (v2), verify_credentials, delete_status, GoToSocialError
  • config.py: gt_social_url/token/visibility/format/channels (+ содержимое .env, .env.example)
  • channels.py: resolve_channels (TG) + resolve_gotosocial_channels (fediverse из VESTI_GTS_CHANNELS или gt:-каналы из запроса)
  • main.py: _publish_gotosocial (upload_media → post_status; status_id в ответе), healthz-блок gotosocial, dry_run для fediverse
  • Аккаунт vesti создан на GtS (admin account create, confirmed+approved), НЕ от имени kpa39l — по запросу пользователя отдельный бот-аккаунт
  • OAuth-приложение vesti-publisher (POST /api/v1/apps, scopes write write:media); токен для vesti: вставлен в БД tokens (user_id из users, scope read write) + записан в .env
  • Тесты e2e: dry_run (fediverse), реальный текст → пост, медиа → пост с картинкой, delete → 200; посты-тесты удалены
  • Фикс: message_id=0 для GtS (id — ULID-строка) → добавлен status_id: str

Сделано ранее (own-content-hub)

  • Миграция БД (migrate_own.py): is_own, is_own_canonical, sources.own, published.distributed_dirs
  • sources.yaml: +dedinit (own: true), синк
  • Краулер own-семантика; классификатор critical + мультинаправления; карточка с атрибуцией; веб «Свои»/бейдж/чекбоксы; бандлы origin:own
  • Тест e2e (dry-run): approve с dirs [linux,ai] → /published с distributed_dirs

В работе / Следующие шаги

  • Бэкфилл своего канала @dedinit (задача 2.3) — ЗАВЕРШЁН (2026-09-10): fetched=1040, max_post_id=1092, 846 постов is_own=1 в БД
  • publisher: медиа при Docker-запуске — исправлено: compose маунтит ../../media/media:/srv/publisher/media:ro
  • systemd vesti-web.service + cron per-source + тихий watchdog — юнит установлен (подхватится при перезагрузке); cron: vesti-crawler-all-sources (*/30, тихий) + vesti-watchdog (*/15, Telegram при проблемах)
  • Классификатор по неклассифицированным — запущен (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-репо (2026-09-13): gitverse kpa39l/vesti (main) + gitea-зеркало bigbox
  • openspec archive web-изменений (deexternalize-web-assets, web-render-markdown, web-format-datetime, web-list-filter-no-js) и publisher-service / tg-crawler-publisher-prototype (после подтверждения)

Как запустить / проверить

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":"<b>Тест</b>","direction":"linux","lang":"ru"}}'
#   fediverse (GoToSocial @vesti@dedinit.ru):
curl -s -X POST http://127.0.0.1:8410/api/v1/publish \
  -H 'Content-Type: application/json' \
  -d '{"card":{"text":"Тест fediverse","direction":"linux"},"channels":["gt:@vesti@dedinit.ru"]}'
#   dry_run (эмуляция без внешних вызовов): добавить "dry_run":true
#   (старый 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

Сделано (сессия закрытия 2026-09-13)

  • Классификация направлений приведена к канону linux, tech, politics, games, electronics, llm (AGENT.MD): keywords.py (add politics/llm, dev→tech, убрать media/ai, +golang), classify.py (промпт из DIRECTIONS_CANON + LLM-fallback при dict-miss: словарь не дал → всё равно LLM), web/app.py (фильтр из канона). Change unify-directions архивирован.
  • Переклассификация: 503 (466 своих + 37 внешних) + 206 (сброшены dev/media/ai) → 966/1019 классифицированы; без direction осталось 53 (шум/личное: музыка, поздравления — не новости). Старые мусорные dev/media/ai устранены.
  • git-репозиторий /opt/vesti: origin gitverse.ru kpa39l/vesti (main, private), зеркало gitea bigbox estorozhenko/vesti (mirror 8h). .gitignore: backups/ media/ db/ bundles/ logs/ секреты.
  • Тест публикации с медиа сквозь веб: сообщения ушли в @dedinit_vesti (11/12) и удалены. Обнаружен и исправлен фейковый dry_run в publisher (поле уехало в PublishResponse вместо PublishRequest; собыл 500). Change publisher-dry-run-fix архивирован: dry_run=true → эмуляция без TG.
  • OpenSpec changes архивированы (8): fix-approve-dirn, web-media-preview, web-format-datetime, web-list-filter-no-js, deexternalize-web-assets, web-render-markdown, publisher-dry-run-fix, unify-directions.
  • Реализован change candidates-mail-ui (двухпанельный интерфейс кандидатов как почтовый клиент): левая панель — список с группировкой (source/date/status, свертываемые
    ) + чекбоксы bulk (approve/reject/reject-old), правая — детали поста (меню действий, комментарий, черновик пересказа, оригинал с медиа). Роуты: POST /candidates/select, /candidates/bulk, /posts/{id}/comment, /posts/{id}/reclassify (qwen), /posts/{id}/rewrite (qwen). БД: добавлены comment TEXT, rewritten_text TEXT. Без JS (только данные формы). Протестировано TestClient (группировка, bulk-reject, comment, reclassify, rewrite, поиск) — зелёные. Change архивирован (2026-09-13).
  • Change rewrite-as-author (2026-09-15): блок «✍️ Переписать» в карточке кандидата — убран readonly (textarea теперь редактируемая + кнопка «💾 Сохранить пересказ» → POST /posts/{id}/rewrite-save), при публикации пересказ (rewritten_text) приоритетнее исходного текста; карточка с пересказом подписывается «✍️ Дед в АйТи (@dedinit) · Источник: …» (от имени автора канала, без «🔗 Оригинал»). Посты без пересказа — как раньше.

Текущие задачи (из TODO, открытые)

  • Интерфейс кандидатов как почтовый клиент (2 панели, группировка, bulk) — ✅ реализован в change candidates-mail-ui (см. выше).
  • Ручное указание/изменение направления (значок «?» у неклассифицированных)
  • Переписывание новостей от себя (LLM-пересказ, персональный контент)
  • Извлечение источников из чужих постов + сервис поиска источников в интернете (дообогащение ТОЛЬКО одобренных)
  • Страница управления источниками (CRUD + фильтр по типу: Telegram, RSS, почта, сайты, Twitter)

Сделано (сессия кандидаты-фиксы 2026-09-14)

Сессия посвящена трём багам страницы кандидатов, все исправлены через OpenSpec-процесс.

  • change candidates-only-external-new (коммит acb49cc): в списке кандидатов показывались одобренные к публикации посты и «все посты как свои».
    • Причина: в _fetch_candidates при пустом фильтре статуса НЕ было фильтрации p.status, поэтому в список попадали published (6) и rejected (3); а 846 постов обратного филла своего канала @dedinit (is_own=1, status=new) при группировке по дате заполняли группы 04-06.2026 полностью и выглядели «своими» (⭐), хотя пользователь их не одобрял.
    • Решение (по выбору пользователя, вариант B): жёстко p.is_own=0 AND p.status='new' — кандидаты = только внешние new-посты. Свои посты канала не кандидаты, управляются через fan-out/«Свои».
    • Итог: 164→163 поста, все внешние new; группы 04-06.2026 (чисто свои) исчезли; 0 бейджей «СВОЙ».
  • change keep-candidates-context (коммит 0999424): после «Отклонить» при группировке по дате открывалась пустая страница («нет кандидатов по фильтру») и фильтр сбрасывался на «источник».
    • Баг 1: жёсткое p.status='new' конфликтовало с явным ?status=rejected (new AND rejected = пусто).
    • Баг 2: редирект после reject жёстко шёл на /candidates?status=rejected без group_by.
    • Решение: жёсткое new только при пустом статусе; явный статус — единственный фильтр. Хелпер _cand_back() строит URL возврата с сохранением group_by/direction/own/q; во все формы правой панели (approve/reclassify/rewrite/reject/comment) добавлены скрытые поля контекста.
    • Итог: ?status=rejected показывает 4 поста (не пусто); после reject остаёшься в своей группировке.

Итоговое состояние страницы кандидатов (после сессии)

  • Список: только внешние new-посты (сейчас 163), без published/rejected и без своих.
  • Группировка (date/source/status) и фильтры сохраняются после любых действий из правой панели.
  • Тихие OpenSpec-change: candidates-only-external-new, keep-candidates-context — задачи [x], валидны; архивировать после подтверждения.
  • Правый блок действий: кнопки «🤖 Обработать моделью / ✍️ Переписать / 🚫 Отклонить» вынесены в ОДИН ряд НАД полем комментария, «✅ Опубликовать» — в начале approve-формы, поле комментария + «💾 Сохранить комментарий» — ниже (change candidates-buttons-order).
  • Список кандидатов: флаг «прочитан» — непрочитанные карточки жирные, выбранный пост — светло-голубая заливка (класс active-row), открытие поста помечает is_read=1; группировка по умолчанию — «дата» (хронология получения) (change candidates-read-flag).
  • Пайплайн «Кандидаты → Отобранные → Опубликованные» (change selected-list): новый статус selected; в КАНДИДАТАХ — только «⭐ Отобрать» (single+bulk), вся обработка (классификация/переписывание/комментарий/публикация) — в «⭐ Отобранных» (отдельный список в навбаре, /selected); классификация — только по кнопке «🤖 Обработать моделью» (токены на кандидатов не тратятся; bulk «Обработать» из кандидатов убран).
  • Подготовка своего поста (в «Отобранных»): рядом с оригиналом — поле «✍️ Мой пост» (textarea rewritten_text, ВСЕГДА видно), куда «✍️ Переписать» кладёт черновик модели; текст редактируется и сохраняется (публикация идёт им, а не репостом оригинала); «✅ Опубликовать» и «💾 Сохранить комментарий» — под полем комментария.

Ключевые артефакты

  • /opt/vesti/AGENT.MD — правила проекта (обязательно: все изменения через OpenSpec)
  • /opt/vesti/crawler/rss_crawler.py — RSS/Atom-краулер (feedparser+httpx, etag/modified, trafilatura, rss_state; lwn первый источник)
  • /opt/vesti/crawler/worker.py — фаза 3: параллельная обработка очереди кандидатов (ThreadPoolExecutor, атомарный claim по posts, лимит MAX_CONCURRENT_LLM к Ollama)
  • /opt/vesti/crawler/crawl_sources.py — фаза 1: сбор кандидатов с источников (telegram+rss) в очередь
  • /opt/vesti/web/templates/crawlers.html + /crawlers — статус краулеров (alive/dead, last_fetch, ошибки) и размер очереди (pending/processing/done), кнопки «Запустить воркер» и «Сбросить dead»
  • /opt/vesti/web/templates/sources.html + /sources — страница управления источниками (add/edit/toggle-пауза/delete с confirm; источник истины — sources.yaml → полный ре-синк в БД; каскад при удалении: посты остаются с source_id=NULL)
  • /opt/vesti/openspec/changes/rss-crawler/ — OpenSpec change RSS-краулера (proposal/design/tasks/spec)
  • /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 — СДЕЛАН (2026-09-13): gitverse kpa39l/vesti (main) + gitea-зеркало bigbox (8h)
  • systemd daemon-reload на bigbox зависает (внешняя проблема, не vesti) — юнит подхватится при перезагрузке
  • Качество классификации — ПРИВЕДЕНО К КАНОНУ (2026-09-13): 966/1019 с direction; 53 без — шум/личное