mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
235 lines
41 KiB
Markdown
235 lines
41 KiB
Markdown
# VESTI — Статус
|
||
|
||
Обновлено: 2026-09-20 (активны: post-translate, archive-navigation, extract-post-sources-остаток, rich-repost-card-аудит)
|
||
<!--
|
||
История обновлений:
|
||
2026-09-20 — Приняты в контекст незакоммиченные наработки (проверена реализация):
|
||
extract-post-sources (backend: sources/extract.py, _post_sources, POST /sources/add-from-post),
|
||
manual-direction (impl_reclassify пишет manual_direction без LLM, select в selected.html),
|
||
crawler-очередь (crawler/worker.py ThreadPoolExecutor, crawl_sources.py, /crawlers + crawlers.html).
|
||
Код в HEAD (кроме untracked: worker.py, crawl_sources.py, extract.py, crawlers.html, CONTEXT.md, docs/).
|
||
Не коммитил (по указанию пользователя). Добавлены задачи: выполненные и планируемые.
|
||
2026-09-18 — post-translate: кнопка «🌐 Перевести» в карточках кандидатов и отобранных.
|
||
Перевод на русский локальной моделью (qwen3:8b-nothink), результат в
|
||
posts.translated_text (колонка добавляется идемпотентно при старте).
|
||
«Оригинальный пост» стал закладкой; при готовом переводе рядом появляется
|
||
закладка «Перевод» (активна по умолчанию), переключение без перезагрузки
|
||
(vanilla JS в scripts-блоке). Повторный клик «Перевести» токены не тратит.
|
||
call_ollama из classifier не подошла (жёстко парсит ответ как JSON) —
|
||
перевод идёт прямым запросом к Ollama /v1/chat/completions.
|
||
2026-09-17 — keep-bulk-checkboxes: фикс чекбоксов bulk-выбора. Отметки постов сохраняются
|
||
в sessionStorage (vesti_bulk_candidates / vesti_bulk_selected) и восстанавливаются
|
||
при открытии поста (?selected=N); badge sel-count пересчитывается; при submit
|
||
bulk-формы отметки очищаются. Найден баг: чекбоксы вне <form> (form="bulk-form"),
|
||
селектор #bulk-form input[name="ids"] не находил их → заменён на input[name="ids"].
|
||
Коммит c8fe5f7, запушен в gitverse.
|
||
2026-09-16 — address-field-enhancement ЗААРХИВИРОВАН (openspec/changes/archive/2026-09-16-address-field-enhancement/).
|
||
(вместо url/channel/feed_url), парсинг по типу crawler; кнопка «Найти» получает
|
||
название канала из TG API (Telethon get_entity; RSS — feedparser title) и slug из адреса;
|
||
direction — datalist из реальных значений (DIRECTIONS + БД) + свободный ввод.
|
||
2026-09-16 — fix-sources-add-channel: в форме добавления источника появилось поле channel
|
||
(required для telegram через JS), в таблице у TG-источников виден канал.
|
||
Причина бага: поле channel отсутствовало в UI, хотя валидатор требует его.
|
||
Заодно: web/templates/sources.html и crawlers.html НЕ были в git (untracked) — sources.html добавлен.
|
||
2026-09-15 — rss-formatting-preserve: html_to_text/extract_full_text сохраняют абзацы (\n\n);
|
||
safe_html (bleach) для предпросмотра RSS-текстов; пересобраны старые посты
|
||
(omarchy 25/25, bleepingcomputer 16/16); lwn частично (403/429 — при след. крауле)
|
||
2026-09-15 — change selected-layout-stay: «⭐ Отобрать» (single+bulk) больше не уводит на /selected — редирект на /candidates с сохранением группировки/фильтров (post_select → src="candidates", bulk select → back). Отобранные: панели на всю высоту окна (calc(100vh - 160px) вместо 82vh), резюме — отдельным блоком сразу под кнопками «Обработать моделью», редактор «Мой пост» + оригинал растянуты на всю оставшуюся высоту, комментарий + «Опубликовать»/«Сохранить комментарий» — в самом низу (border-top). Пустое место внизу убрано.
|
||
2026-09-14 — кандидаты: после reject/approve/comment/reclassify/rewrite сохраняется group_by и фильтры (скрытые поля форм + _cand_back); ?status=rejected больше не даёт пустой список (жёсткое status='new' только по умолчанию)
|
||
2026-09-14 — кандидаты: в списке только внешние new-посты (is_own=0 AND status='new' в _fetch_candidates); свои посты канала (is_own=1) и published/rejected больше не показываются как кандидаты
|
||
2026-09-14 — фикс published-страницы: заголовок перекрывался fixed-top меню (padding-top 1.5rem→4.5rem в base.html); на карточках добавлен ID поста (#{{p.id}} · направление)
|
||
2026-09-14 — фикс бага: статус группы в _fetch_candidates брался от первого поста → при group_by=date группа «Ранее на этой неделе» выглядела опубликованной; теперь доминирующий по группе (new > rejected > published)
|
||
2026-09-13 — fediverse: publisher умеет постить в GoToSocial (@vesti@dedinit.ru)
|
||
2026-09-13 — publisher снова на Docker; systemd1 ожил
|
||
2026-09-12 — publisher → systemd user-юнит (костыль, пока сломан systemd1)
|
||
2026-09-11 — веб-интерфейс: 4 openspec change (CDN/htmx, markdown, даты, фильтр без CDN)
|
||
-->
|
||
|
||
## Текущее состояние
|
||
- **ПРИНЯТЫ В КОНТЕКСТ НЕЗАКОММИЧЕННЫЕ НАРАБОТКИ** (2026-09-20, проверена реализация):
|
||
- **extract-post-sources** — backend готов и в HEAD: `sources/extract.py` (extract_sources_from_text: Telegram-каналы + сайты, дедуп, лимит 5), `_post_sources` в app.py, `POST /sources/add-from-post`. **Остаток**: блок «Источники из поста» в candidates.html/selected.html НЕ добавлен (задача 4 в tasks.md открыта). Change валиден.
|
||
- **manual-direction** — готов и в HEAD: `post_reclassify` принимает `manual_direction`, `impl_reclassify` пишет его в БД без LLM; select направлений в selected.html. Change валиден. (Пост 1086 → tech, проверено автором.)
|
||
- **crawler-очередь** — worker.py (ThreadPoolExecutor, атомарный claim по posts, LLM-семафор к Ollama) + crawl_sources.py (фаза 1: сбор кандидатов) + страница /crawlers (статус alive/dead, очередь, кнопки «Запустить воркер»/«Сбросить dead»). **Файлы UNTRACKED** (worker.py, crawl_sources.py, crawlers.html, sources/extract.py, CONTEXT.md, docs/) — не коммитил (указание пользователя).
|
||
- **archive-navigation** — ТОЛЬКО ПЛАН (proposal/design/tasks + CONTEXT.md глоссарий): дерево годов/месяцев в /published, роут /published с year/month/page, пагинация по 10, правая колонка sticky, collapse на мобильных. Реализация НЕ начата (все задачи `[ ]`, /published — плоский список).
|
||
- **rich-repost-card** — tasks 0/13 (не начат), хотя формат публикации уже живёт в publisher/card.py. Требует аудита: проставить статус/закрыть или доделать.
|
||
- **post-translate — В РАЗРАБОТКЕ** (2026-09-18): в карточках кандидатов и отобранных появилась кнопка **«🌐 Перевести»** — переводит пост на русский локальной моделью (qwen3:8b-nothink, Ollama). Результат сохраняется в `posts.translated_text` (новые посты — колонка добавляется при старте веба), кнопка после перевода скрывается, вместо неё — закладка «Перевод» рядом с «Оригинал» (активна по умолчанию, переключение без перезагрузки на vanilla JS). Повторный клик токены не тратит. Ошибки модели — редирект с `?error=translate:...`. Тест e2e на реальной модели пройден (пост 123: «Kali Purple Essential Training»).
|
||
- **keep-bulk-checkboxes — ЗААРХИВИРОВАН И ЗАПУШЕН** (2026-09-17, коммит c8fe5f7): проверки массового выбора постов сохраняются между перезагрузками страницы при навигации по постам. Отметки хранятся в sessionStorage браузера (`vesti_bulk_candidates` / `vesti_bulk_selected`); при открытии поста (?selected=N) восстановленные чекбоксы остаются отмеченными, badge sel-count пересчитывается; при submit bulk-формы (Отобрать/Отклонить/Обработать/Опубликовать) отметки очищаются. Изоляция /candidates и /selected независимыми ключами. Попутно исправлен скрытый баг: чекбоксы лежат вне `<form>` (атрибут form="bulk-form") → селектор `#bulk-form input[name="ids"]` не находил их; заменён на глобальный `input[name="ids"]`. Изменены только web/templates/candidates.html и selected.html (frontend, рестарт не нужен).
|
||
- **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/<file>, файл media/media/<file>; с 2026-09-12 краулер качает в media/<file>, для старых — 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/<file>, а media_path в БД = media/<file> → publisher не находил файл, медиа молча не публиковалось. Теперь скачивание в MEDIA_DIR/<basename> (= media/<file>). Для старых постов — fallback в make_card (пробует media/ и media/media/)
|
||
- [x] telegram.py: добавлен delete_message (для отмены/чистки тестовых)
|
||
- [x] **Проверено на реальном канале**: publish ушёл в @dedinit_vesti (message_id 6), текст с <b>& доставлен как есть, без превью; тестовые сообщения удалены (delete_message ok). Канал чист
|
||
- [x] **publisher временно работает как локальный uvicorn** (127.0.0.1:8410, PID 807769) — Docker недоступен из-за зависшего systemd1 на хосте (внешняя проблема, не vesti)
|
||
|
||
## Сделано (сессия gotosocial-publisher 2026-09-13)
|
||
- [x] **OpenSpec change `gotosocial-publisher`** — proposal/design/tasks/specs/gotosocial-publisher, `openspec validate` чисто
|
||
- [x] services/publisher/app/gotosocial.py — клиент GtS (httpx): post_status (format=markdown), upload_media (v2), verify_credentials, delete_status, GoToSocialError
|
||
- [x] config.py: gt_social_url/token/visibility/format/channels (+ содержимое .env, .env.example)
|
||
- [x] channels.py: resolve_channels (TG) + resolve_gotosocial_channels (fediverse из VESTI_GTS_CHANNELS или `gt:`-каналы из запроса)
|
||
- [x] main.py: `_publish_gotosocial` (upload_media → post_status; status_id в ответе), healthz-блок gotosocial, dry_run для fediverse
|
||
- [x] **Аккаунт vesti создан** на GtS (admin account create, confirmed+approved), НЕ от имени kpa39l — по запросу пользователя отдельный бот-аккаунт
|
||
- [x] **OAuth-приложение vesti-publisher** (POST /api/v1/apps, scopes write write:media); токен для vesti: вставлен в БД tokens (user_id из users, scope read write) + записан в .env
|
||
- [x] **Тесты e2e**: dry_run (fediverse), реальный текст → пост, медиа → пост с картинкой, delete → 200; посты-тесты удалены
|
||
- [x] **Фикс**: message_id=0 для GtS (id — ULID-строка) → добавлен `status_id: str`
|
||
|
||
## Сделано ранее (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
|
||
|
||
## В работе / Следующие шаги
|
||
- [ ] **archive-navigation** (2026-09-20, план готов): дерево годов/месяцев в /published — роут с year/month/page, `_archive_tree(conn)` GROUP BY, шаблон col-lg-3 sticky, collapse на мобильных, пагинация по 10 (tasks.md — 8 задач)
|
||
- [ ] **extract-post-sources — остаток**: блок «Источники из поста» в candidates.html/selected.html (backend готов, шаблоны нет)
|
||
- [ ] **rich-repost-card — аудит**: tasks 0/13, формат уже в publisher/card.py — проставить статус/закрыть или доделать
|
||
- [ ] **Коммит незакоммиченных наработок** (ждёт решения пользователя): worker.py, crawl_sources.py, crawlers.html, sources/extract.py, CONTEXT.md, docs/, openspec changes, правки classifier/config/backup.sh/card.py/.env.example/AGENT*.md
|
||
- [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 после подтверждения), архивировать
|
||
- [x] ~~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 (после подтверждения)
|
||
|
||
## Как запустить / проверить
|
||
```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":"<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)
|
||
- [x] **Классификация направлений приведена к канону** `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` архивирован.
|
||
- [x] **Переклассификация**: 503 (466 своих + 37 внешних) + 206 (сброшены dev/media/ai) → 966/1019 классифицированы; без direction осталось 53 (шум/личное: музыка, поздравления — не новости). Старые мусорные dev/media/ai устранены.
|
||
- [x] **git-репозиторий** /opt/vesti: origin gitverse.ru `kpa39l/vesti` (main, private), зеркало gitea bigbox `estorozhenko/vesti` (mirror 8h). .gitignore: backups/ media/ db/ bundles/ logs/ секреты.
|
||
- [x] **Тест публикации с медиа сквозь веб**: сообщения ушли в @dedinit_vesti (11/12) и удалены. Обнаружен и исправлен фейковый `dry_run` в publisher (поле уехало в PublishResponse вместо PublishRequest; собыл 500). Change `publisher-dry-run-fix` архивирован: `dry_run=true` → эмуляция без TG.
|
||
- [x] **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.
|
||
- [x] **Реализован change `candidates-mail-ui`** (двухпанельный интерфейс кандидатов как почтовый клиент): левая панель — список с группировкой (source/date/status, свертываемые <details>) + чекбоксы 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).
|
||
- [x] **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-процесс.
|
||
|
||
- [x] **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 бейджей «СВОЙ».
|
||
- [x] **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/<file>)
|
||
- **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 без — шум/личное |