Перейти к содержанию

Архитектура

Peter View - три процесса в одной внутренней сети Compose и том с данными. С хоста открыт один порт nginx. Браузер не ходит на FastAPI и LanguageTool напрямую.

Состав стека

Браузер
  → nginx (статика и /api, порт на хосте)
      → FastAPI
          → Vale, pymorphy3
          → LanguageTool (отдельный контейнер)
          → модель, если задан адрес
          → том backend-data
Контейнер Образ / сборка Процесс Память в Compose С хоста
frontend nginx:alpine, frontend/public nginx, порт 80 внутри без лимита PROOFREADER_PORT, по умолчанию 3080
backend python:3.12-slim, Vale 3.9.1, uvicorn FastAPI app.main:app, порт 8000 до 2 ГБ нет
languagetool erikvl87/languagetool, только ru HTTP /v2/check на 8010 1.5 ГБ, куча 512 МБ–1 ГБ нет

Сеть internal, драйвер bridge. LanguageTool должен пройти healthcheck (до 90 с на старт), затем backend, затем frontend.

Языковая модель в стек не входит. Backend вызывает адрес из «Настроек» или из LLM_*. Пустой адрес: слой модели не вызывается, Vale / морфология / LanguageTool остаются.

Опционально PROOFREADER_CORP_PROXY=true: контейнер proxy-bridge (socat) прокидывает корпоративный прокси хоста, backend ходит в модель через host.docker.internal.

Зачем три процесса

LanguageTool - отдельная JVM с длинным стартом. Если держать её в том же процессе, что и API, падение грамматики останавливает вход и гайды. Healthcheck специально растянут: перезапуск каждые десять секунд только мешает разгону.

Данные лежат в томе, не в образе. Обновление контейнера не должно стирать пользователей и гайды.

Интерфейс - статические файлы. Node в производственной среде не нужен. Пункты меню backend отдаёт в /api/config; флаг FEATURE_* меняет меню без пересборки фронта.

Путь запроса из браузера

  1. Браузер открывает http://хост:3080. nginx отдаёт index.html, скрипты ES-модулей, стили.
  2. Маршруты страниц - hash: #/check, #/review, #/guides. Обновление страницы не спрашивает сервер о пути.
  3. Все данные идут на тот же origin: /api/.... nginx проксирует на backend:8000. Загрузка до 50 МБ, таймаут 600 с.
  4. Проверка и стрим воркеров идут как SSE: /api/jobs/{id}/stream и устаревший /api/check-url. Для них nginx отключает буферизацию (X-Accel-Buffering: no), иначе карточки появятся пачкой в конце.
  5. Cookie сессии proofreader_session HttpOnly, SameSite=lax. Флаг Secure: PROOFREADER_COOKIE_SECURE. Живые сессии хранятся в памяти backend. Рестарт контейнера разлогинивает всех; users.json при этом остаётся.

CSP на статике: скрипты только 'self'. Исключение: /api/docs и /api/redoc, если PROOFREADER_DOCS=true.

Как устроена проверка в интерфейсе

Раздел «Вычитка» не вызывает /api/check. Он ставит задачу:

  1. Источник разбирается в блоки документа: DOCX / TXT / HTML / MD, либо одна страница по URL (parse_url). Обход сайта на 200 страниц (/api/check-url) в этом экране не используется.
  2. POST /api/jobs с галочками языка, гайда, терминов, идентификатором Style Guide и необязательной инструкцией.
  3. Задача живёт в памяти и дублируется в jobs.db. Рестарт помечает незавершённые задачи ошибкой «Прервано рестартом сервера».
  4. Браузер подписан на SSE и по окончании забирает отчёт GET /api/jobs/{id}/report.
  5. Отчёт пишется в историю (stats.db).

Внутри задачи работает пайплайн PIPELINE_VERSION=v2 (v1 остаётся для отката). Сначала детерминированные движки, затем при наличии модели - проходы LLM. Подробности: как проходит проверка.

Прямые POST /api/check, /api/check-text, /api/check-url остаются в HTTP API: те же Vale / морфология / LanguageTool без очереди jobs. Их вызывает не экран «Вычитка», а отчёт xlsx и краулер.

Backend: модули

Код сервера: backend/app/.

Модуль Роль
main.py Маршруты HTTP
auth.py, oidc.py Пароль, сессия, вход организации
features.py Флаги разделов, 404 если выключено
extractors.py Текст из файла и HTML
checker.py Параллельно Vale, pymorphy3, LanguageTool
vale_runner.py Vale CLI, JSON
custom_checks.py Морфология: пассив, «Вы», длина предложения, согласование
lt_client.py HTTP к контейнеру LanguageTool
net_guard.py SSRF при загрузке URL
crawler.py Обход сайта, до 200 страниц
llm/jobs.py Очередь проверок, SSE
llm/pipeline.py, pipeline_v2.py Сборка отчёта и проходы модели
llm/styleguide_store.py YAML гайдов на томе
llm/settings.py Адрес модели, перекрывает .env
llm/repo_store.py Репозиторий документов
llm/watch_store.py, watch_run.py Наблюдение за страницами
backups.py tar тома
audit.py Журнал действий администратора

Интерфейс: frontend/public/js/, по файлу на раздел (check.js, guides.js, users.js, …). Оболочка и вход: app.js. Общие запросы: shared.js. Строки ru/en: frontend/public/i18n/.

Данные

Том backend-data монтируется в /app/data. Состав файлов: хранилище. Резервная копия снимает этот каталог в ./backups на хосте.

Сеть и границы

Загрузка страницы по ссылке проходит net_guard: схема только http(s), запрещены loopback, link-local (в том числе 169.254.169.254), multicast. Адреса RFC1918 по умолчанию разрешены (PROOFREADER_SSRF_ALLOW_PRIVATE=true), потому что документация часто живёт во внутренней сети. На узле из интернета поставьте false.

Модель и эмбеддинги - исходящие запросы backend по адресу, который задал администратор. На GitHub и на сайт автора проверка ничего не отправляет.

HTTP-группы: справка API. Команды Compose: справка.