# proxy-ws Rust/Axum-шлюз, стоящий между фронтендом (Next.js) и PocketBase (CMS/БД). Независимый сервис — не зависит от `matrix-engine` или самого фронта, общается с ними только по HTTP. У сервиса две обязанности: 1. **AI-модерация гостевой книги** — принимает комментарии с фронта, прогоняет никнейм и текст через ONNX-классификатор токсичности и, если всё чисто, форвардит запись в PocketBase. 2. **Backend для спидтеста** — отдаёт несжимаемый блоб на скачивание, принимает и считает байты на загрузку, отвечает на ping — без сохранения каких-либо данных. ## Эндпоинты | Метод | Путь | Назначение | |-------|--------------------------|------------------------------------------------------------------------------| | POST | `/api/comments` | Приём сообщения гостевой книги, ИИ-модерация, форвардинг в PocketBase | | GET | `/api/speedtest/ping` | Возвращает `pong` — для измерения RTT | | GET | `/api/speedtest/download`| Отдаёт статический 25 МБ несжимаемый блоб — для измерения скорости скачивания| | POST | `/api/speedtest/upload` | Принимает тело запроса, считает байты, ничего не сохраняет | Лимит тела запроса — 100 МБ (`DefaultBodyLimit`), нужен именно из-за speedtest upload. ## Как работает модерация Пайплайн для каждого проверяемого текста (`classifier.rs`): ``` текст -> токенизация (tokenizers) -> ONNX-модель (tract, чистый Rust, без Python) -> логиты -> сигмоида -> порог 0.85 ``` Модель — бинарный BERT-подобный классификатор токсичности. Инференс выполняется через `tract-onnx`, поэтому сервису не нужен ни Python, ни какой-либо ML-рантайм на уровне ОС — только сам `.onnx`-файл модели и токенизатор, лежащие в `models/`. Особенности пайплайна: - **Никнейм проверяется всегда**, без исключений. - **Текст сообщения проходит гвард-рейлы** перед инференсом: сообщения из whitelist безобидных слов (`vpn`, `service`, `спасибо`, `тест` и т.п.), короткие (< 4 символов) или не содержащие ни одной буквы — на модель вообще не отправляются и автоматически считаются нетоксичными. Это защищает от false positives на коротких безобидных фразах вроде "спасибо, работает" и экономит инференс. - **Инференс — блокирующая операция**, поэтому выполняется в `tokio::task::spawn_blocking`, чтобы не задерживать поток из пула async-раннтайма. - **Fail-open при ошибках модели**: любая ошибка на любом шаге (токенизация, прогон модели, неожиданная форма выходного тензора) означает, что текст считается нетоксичным и пропускается дальше. Это осознанный компромисс в пользу доступности сервиса — сбой модели не должен ронять или блокировать гостевую книгу целиком. Если хотя бы один из текстов (никнейм или прошедшее гвард-рейлы сообщение) признан токсичным — сервис отвечает `406 Not Acceptable` и ничего не форвардит в PocketBase. ## Переменные окружения | Переменная | Назначение | Значение по умолчанию | |------------------|--------------------------------------------------------|---------------------------------| | `PB_INTERNAL_URL`| Внутренний адрес PocketBase, куда форвардятся комментарии | `http://netrunner-cms:8090` | ## Как собрать и запустить локально ### Через Cargo Нужны файлы модели в `models/` (`model.onnx`, `tokenizer.json` и сопутствующие конфиги — уже лежат в репозитории). ```bash cd proxy-ws cargo run ``` Сервис поднимется на `0.0.0.0:3001`. ### Через Docker Dockerfile лежит внутри `proxy-ws/`, но собирать образ нужно из корня репозитория (контекст сборки — вся папка `proxy-ws`): ```bash docker build -t proxy-ws ./proxy-ws docker run -p 3001:3001 -e PB_INTERNAL_URL=http://netrunner-cms:8090 proxy-ws ``` Сборка двухстадийная (Alpine): сначала кэшируются зависимости отдельной компиляцией фейкового `main.rs`, затем собирается реальный код — итоговый образ содержит только бинарник и `models/`, без Rust-тулчейна.