No description
  • Python 91.1%
  • TypeScript 4.9%
  • JavaScript 1.8%
  • HTML 0.9%
  • Dockerfile 0.6%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
xfx1337 a9ed0a73d3 Добавить страницу /preview — лента глазами клиента
Во фронте была только админка под сессией: посмотреть, что видит держатель
ключа на чтение, было негде, а отдать ключ тестировщикам без такой страницы
нечего. Страница спрашивает ключ, держит его в localStorage браузера и ходит
только в /api/v1/feed и /api/v1/taxonomy.

Фильтры по осям строятся из таксономии, счётчики берутся из фасетов ответа.
Ответы на устаревшие запросы отбрасываются по номеру: при быстрых кликах по
фильтрам они приходят не в том порядке, в каком уходили.

Разметка markdown разбирается на месте, а картинки тянутся запросом и
подставляются как data: — CSP страницы запрещает и внешние скрипты, и blob:,
а заголовок с ключом в <img src> не поставить.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 13:24:22 +03:00
.serena Переработана архитектура 2026-09-06 12:29:31 +03:00
apps/web Добавить страницу /preview — лента глазами клиента 2026-09-12 13:24:22 +03:00
contracts Переработана архитектура 2026-09-06 12:29:31 +03:00
docs Описать выкат за чужим прокси и обязательную регистрацию источника 2026-09-12 13:14:40 +03:00
infra Переработана архитектура 2026-09-06 12:29:31 +03:00
packages/python/contracts Переработана архитектура 2026-09-06 12:29:31 +03:00
services Не писать стандартный порт в адрес загрузок и открыть настройки парсера 2026-09-12 13:13:50 +03:00
tools fix: Some things have become more majestic 2026-09-06 20:08:14 +03:00
.dockerignore Переработана архитектура 2026-09-06 12:29:31 +03:00
.env.example Не писать стандартный порт в адрес загрузок и открыть настройки парсера 2026-09-12 13:13:50 +03:00
.gitattributes Переработана архитектура 2026-09-06 12:29:31 +03:00
.gitignore Переработана архитектура 2026-09-06 12:29:31 +03:00
AGENTS.md Переработана архитектура 2026-09-06 12:29:31 +03:00
docker-compose.yml Не писать стандартный порт в адрес загрузок и открыть настройки парсера 2026-09-12 13:13:50 +03:00
justfile Переработана архитектура 2026-09-06 12:29:31 +03:00
Makefile Переработана архитектура 2026-09-06 12:29:31 +03:00
README.md Добавить страницу /preview — лента глазами клиента 2026-09-12 13:24:22 +03:00

3rd-news

Университетский агрегатор новостей: принимает оригиналы, асинхронно получает классификацию от внешних сервисов, нормализует результат и публикует закрытую ленту, RSS и API. Версия 2 предназначена для новой базы. Старые базы и Docker volumes автоматически не изменяются и не удаляются.

Архитектура

  • services/main: один Python-пакет lib и один образ, режимы api, worker-outbox, worker-pipeline, worker-index.
  • services/classifier-*, services/parser-*: самостоятельные внешние программы. Главный сервис знает только их версионированный HTTP/JSON-контракт.
  • packages/python/contracts: общие Python DTO, подписи и SDK. Общий Python-код размещается только внутри packages/python.
  • contracts: машинные схемы; docs/contracts: описание контрактов.
  • apps/web: существующие сценарии админки, перенесённые без редизайна на актуальные React 19, Vite 8 и TypeScript 7. Рядом со сборкой лежит public/preview.html — отдельная страница /preview, которая показывает ленту глазами клиента: спрашивает ключ на чтение и ходит только в /api/v1/feed и /api/v1/taxonomy.
  • infra: Compose, Caddy и инициализация инфраструктуры.

PostgreSQL — источник истины. Outbox записывается в одной транзакции с новостью, NATS JetStream обеспечивает повторную доставку, обработчики проверяют inbox и версии попыток. Meilisearch — восстанавливаемая поисковая проекция. Garage S3 хранит закрытые вложения. Сведения о конкретных источниках и классификаторах задаются регистрациями, а не импортами внутри главного сервиса.

Таксономия динамическая. Мнения и версии сохраняются отдельно; ручные решения не затираются AI. importance = urgency + impact + editorial_priority, каждый компонент 0–100. Начальные правила нейтральны; примеры правил выключены.

Запуск

Требуются Docker Engine и Docker Compose v2. Новая система использует отдельное имя проекта thirdnews-v2, не старое 3rd-news.

docker compose up -d --build
docker compose logs -f api worker-outbox worker-pipeline worker-index

Первый запуск создаёт случайные ключи и пароли в отдельных config volumes. Секреты не встроены в образы и не печатаются в startup logs. Ollama автоматически загружает qwen3:0.6b; первый запуск требует доступа к registry и загрузке модели. Общий AI-классификатор по умолчанию использует OpenAI-совместимый протокол и strict JSON Schema. CPU smoke-профиль задаёт PROVIDER_PROTOCOL=ollama-native, потому что Ollama 0.33 с Qwen3 игнорирует отключение reasoning в OpenAI /v1 и превышает deadline. Адаптер вызывает /api/chat с think=false и той же JSON Schema; это явное ограничение проверочного стенда, а не контракт production-провайдера. CPU-профиль служит воспроизводимым стендом, а не обещанием production SLO.

Админка: http://localhost:8080. Начальный пользователь: admin@example.edu. Получить сгенерированный пароль локально:

docker compose exec api thirdnews-bootstrap-password
docker compose up -d --scale worker-pipeline=4
docker compose run --rm migrate
docker compose down

Миграции выполняет отдельный процесс; API-реплики не запускают Alembic. Команда остановки не удаляет volumes. Файловая выдача проходит проверку прав на новость; порт 8081 принимает только подписанные PUT uploads.

Для TLS предусмотрен infra/compose.production.yml: задать SITE_ADDRESS, UPLOAD_ADDRESS, FILE_PUBLIC_SCHEME=https, FILE_PUBLIC_HOST=<upload-domain>, FILE_PUBLIC_PORT=443, настроить DNS и запустить Compose с обоими файлами. Production overlay открывает только proxy на 80/443; PostgreSQL, NATS, Meilisearch и Garage admin наружу не публикуются.

За уже существующим реверс-прокси

Если на машине уже стоит прокси под другие домены, второй Caddy за первым разносит маршрутизацию по двум местам. Тогда infra/compose.production.yml не нужен, а docker-compose.override.yml рядом с проектом выключает лишнее и публикует сервисы на loopback:

services:
  proxy:
    profiles: [unused]
  api:
    ports: ["127.0.0.1:8110:8000"]
  web:
    networks: [internal, frontdoor]
    ports: ["127.0.0.1:8112:80"]
  file:
    networks: [internal, frontdoor]
    ports: ["127.0.0.1:8111:3900"]
networks:
  frontdoor: {}

profiles: [unused] обязателен: docker compose up без имён сервисов поднимает весь файл. Отдельная сеть нужна потому, что web и file стоят только в internal: true, откуда публикация портов не работает.

Внешний прокси повторяет infra/caddy/Caddyfile: /api/*, /media/* и /health/* — в api, PUT в бакет — в Garage, остальное — в SPA. Префикс пути у загрузок не срезать и Host не подменять: они входят в подпись S3. Отдельное DNS-имя под загрузки при этом не требуется — достаточно указать FILE_PUBLIC_HOST тем же доменом, потому что маршрут разводится по методу и префиксу бакета.

Локальная модель отключается тем же способом: ollama и model-init в profiles: [unused], classifier-ai с depends_on: !reset {}, а PROVIDER_PROTOCOL=openai с OPENAI_* направляет классификацию во внешний совместимый API.

Контракты

  • POST /api/v1/news и /news/batch → 202, идентификатор submission.
  • Обязательны source + external_id либо стабильный ключ идемпотентности. Повтор с другим содержимым даёт конфликт; одинаковый текст не является ключом.
  • GET /api/v1/submissions/{id} — состояние обработки.
  • POST /api/v1/uploads/presign, /uploads/complete — безопасная загрузка.
  • GET /api/v1/feed, /news/{id}, /taxonomy, /rss.xml — закрытая выдача.
  • /api/v1/admin/* — редактура, таксономия, источники, ключи и классификаторы.
  • /health/live, /health/ready — состояние процесса и ресурсов.

Лента, RSS и медиа требуют авторизации; API-ключи передаются заголовками, не query string. После ограничения видимости устаревшая поисковая проекция не выдаётся. Классификаторы получают подписанные задания с ограничением осей; повторы и поздние callbacks не меняют завершённую или новую попытку.

Файлы с текстом обрабатываются; OCR и транскрибации нет. Невалидные AI-ответы не публикуются автоматически. Сырые AI payloads защищены отдельным ключом и удаляются через 30 дней; проверенные результаты и метаданные сохраняются.

Разработка и проверки

Python 3.14, только uv; HTTP через aiohttp, без HTTPX. Каждый сервис имеет свои pyproject.toml, uv.lock, .venv. Factory, lifespan, управление ресурсами, request ID, ошибки и health endpoints следуют исходному примеру backend.

make sync
make test
make lint
make fmt
make integration
make mutation
make audit
make web

Те же цели доступны через just. Форматирование в lint проверяется без изменений; fmt исправляет код. Frontend использует только bun. Интеграционные тесты требуют отдельного тестового стенда; не указывайте им рабочую базу. Результаты проверок и ограничения фиксируются в docs/rewrite-report.md.

Новые интерфейсы админки, OCR/транскрибация и доставка уведомлений в эту версию не входят. Подписки добавляются позднее через событийные контракты.