- Python 91.1%
- TypeScript 4.9%
- JavaScript 1.8%
- HTML 0.9%
- Dockerfile 0.6%
- Other 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Во фронте была только админка под сессией: посмотреть, что видит держатель ключа на чтение, было негде, а отдать ключ тестировщикам без такой страницы нечего. Страница спрашивает ключ, держит его в localStorage браузера и ходит только в /api/v1/feed и /api/v1/taxonomy. Фильтры по осям строятся из таксономии, счётчики берутся из фасетов ответа. Ответы на устаревшие запросы отбрасываются по номеру: при быстрых кликах по фильтрам они приходят не в том порядке, в каком уходили. Разметка markdown разбирается на месте, а картинки тянутся запросом и подставляются как data: — CSP страницы запрещает и внешние скрипты, и blob:, а заголовок с ключом в <img src> не поставить. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
| .serena | ||
| apps/web | ||
| contracts | ||
| docs | ||
| infra | ||
| packages/python/contracts | ||
| services | ||
| tools | ||
| .dockerignore | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| justfile | ||
| Makefile | ||
| README.md | ||
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/транскрибация и доставка уведомлений в эту версию не входят. Подписки добавляются позднее через событийные контракты.