# evening_detective_server Шалон для Go сервисов (имя репо должно быть snake case) Методы http должны быть с префиксом /api Инициализация ```shell make generate go mod tidy docker compose -f docker-compose-db.yml up -d cp .env.example .env mkdir migrations ``` Для генерации protobuf нужен `protoc 36.0` и плагины тех же версий, что проверены в этом репозитории: `protoc-gen-go v1.36.11`, `protoc-gen-go-grpc v1.6.2`, `protoc-gen-grpc-gateway v2.30.0` и `protoc-gen-openapiv2 v2.30.0`. Установить плагины можно командой `make install-generate-tools`; каталог `$(go env GOPATH)/bin` должен быть в `PATH`. После установки запуск `make generate` удаляет и пересоздаёт `proto/` и обновляет `cmd/evening_detective_server/main.swagger.json`. Ручное редактирование сгенерированных файлов не требуется. Запуск ```shell make run ``` Создание локального пользователя с полными ролями: ```shell make dev-user ``` По умолчанию: `dev@example.com` / `dev123456`. Значения можно изменить: ```shell make dev-user DEV_USER_EMAIL=test@example.com DEV_USER_PASSWORD=test123456 ``` Сборка ```shell make build-builder make build-linux ``` По умолчанию используется архитектура Docker-среды. Для другой архитектуры укажите, например, `make build-linux GOARCH=arm64` или `make build-linux GOARCH=amd64`. Миграции ```shell goose -dir ./migrations create {migration name} sql make migrate-up ``` Тестирование ```shell make test ``` ## PDF с QR-кодами команд Кнопка «QR команд» на странице управления игрой скачивает `GET /api/games/{id}/teams/qr.pdf` с авторизацией организатора, автора или администратора. Без команд кнопка скрыта; прямой запрос для пустой игры возвращает `FailedPrecondition`. Модуль `internal/modules/team_qr_pdf` формирует A4 в книжной ориентации, 12 карточек (3 × 4) с названиями команд и пунктирными линиями для разрезания. Имя файла: `YYYY-MM-DD_Название игры.pdf`, дата берётся из `games.start_at`. Недопустимые символы имени файла заменяются на `_`. Для рабочих ссылок задайте `APP_BASE_URL` — тот же публичный адрес приложения, который используется существующими QR-кодами фронтенда (`VITE_API_URL`). Пустой или некорректный адрес приводит к ошибке скачивания. PDF не сохраняется в хранилище и отдаётся с `Cache-Control: no-store`. Шрифт DejaVu Sans Condensed встроен в бинарник вместе с лицензией; системные шрифты не нужны. Поддерживается кириллица, длинные названия переносятся и уменьшаются до размера 6 pt. Символы вне BMP (например, emoji) вызывают явную ошибку генерации из-за ограничения PDF-библиотеки. ## Закрывашка сценария Автор (или администратор) сохраняет закрывашку через `PUT /api/scenarios/{id}/conclusion`: ```json {"conclusion":{"text":"Разгадка дела","video":"ending_abc.mp4"}} ``` Видео сначала загружается через `POST /api/files/upload` (MP4/WebM, до 64 МБ); в `video` передаётся возвращённое имя файла. Текст и видео можно задавать отдельно. `{"conclusion":null}` удаляет закрывашку. Она возвращается в `story.conclusion` полного сценария, но не передаётся в историю игроков, включая MCP. Закрывашка переносится вместе с ZIP-архивом; действуют общие лимиты импорта архива (32 МБ на архив и отдельный файл). ## Экспорт игры на автономную площадку Администратор, разработчик или организатор запускает подготовку через `POST /api/games/{ID}/export`, получает состояние через `GET /api/games/{ID}/export` и скачивает готовый ZIP через `GET /api/games/{ID}/export/download` с JWT. Экспорт доступен для игры в черновике без дат проведения и прогресса. Он сохраняет снимок для пакета и показывает в интерфейсе подготовку или офлайн-режим, сохраняя возможность изменений. Изменения после запуска не синхронизируются с готовым пакетом. Ошибка подготовки переводит попытку в `failed`; обрыв скачивания не меняет статус. Повторный запуск готовой попытки возвращает прежний пакет. Используйте только тестовые игры до завершения возврата результатов. UI передачи и импорт реализуются следующими этапами. Контракт, закрытое хранение RustFS, восстановление worker и команды проверки описаны в [`docs/GAME_PACKAGE.md`](docs/GAME_PACKAGE.md#экспорт-со-статусом-офлайн-режима-этап-5). ## Документация и юридические документы - [`docs/TERMS.md`](docs/TERMS.md) — Пользовательское соглашение (оферта); раздаётся сервером по постоянному URL `/api/terms`; - [`docs/PRIVACY.md`](docs/PRIVACY.md) — Политика конфиденциальности и обработки персональных данных (152-ФЗ); раздаётся сервером по постоянному URL `/api/privacy`; - [`docs/CONSENT.md`](docs/CONSENT.md) — текст согласия на обработку ПДн для формы регистрации (URL `/api/consent`); - [`docs/COMPLIANCE.md`](docs/COMPLIANCE.md) — чек-лист приведения сервиса в соответствие с документами. Юридические эндпоинты: - `POST /api/auth/signup` — регистрация; требует `accept_terms: true` и `accept_privacy: true` (факт акцепта фиксируется в таблице `user_agreements`); - `DELETE /api/auth/delete-account` — удаление учётной записи и персональных данных (авторизован JWT, подтверждение паролем в HTTP-заголовке `X-Password`). Структура: - `agent.cordis.yml` — композиция агента (полный набор инструментов, как у пресета `standard`, + персона юриста и скилл-база); - `preset.yml` — отображаемое имя и описание; - `skills/software-law/` — база знаний: `SKILL.md` (правила консультаций) и `references/` (лицензии, авторские права, ПДн, договоры). ## MCP-сервер (встроен в основной сервис) MCP-сервер (Model Context Protocol) встроен в основной бинарь и доступен как HTTP-эндпоинт `/api/mcp` (streamable HTTP) на REST-gateway — чтобы LLM-клиент (Claude Desktop, IDE с MCP-поддержкой и т.п.) мог играть в «Вечерний детектив». Инструменты вызывают игровые сервисы напрямую, отдельный процесс не нужен. Ходы через `make_move` сохраняются с `actions.via_mcp = true`; через REST/gRPC — с `false`. Отметку задаёт сервер, клиент её не передаёт. У старых ходов значение `false`: их источник не отслеживался. Это отметка канала MCP, а не гарантия того, кто фактически играет. Инструменты: | Инструмент | Назначение | |---|---| | `connect` | Подключиться к игре по ссылке `/team-story/{id}?password=...`; возвращает id команды, пароль и текущую историю | | `get_team_story` | История команды: точки, двери, улики, вопросы дела + инфо об игре (по паролю команды, без авторизации) | | `make_move` | Ход команды в точку по её коду; возвращает обновлённую историю (по паролю команды, без авторизации) | | `submit_answers` | Ответы команды на вопросы дела (массив `{question_code, answer}`); возвращает обновлённую историю (по паролю команды, без авторизации) | Ссылку на игру (`/team-story/{id}?password=...`) выдаёт команде организатор личным каналом — например, вместе с паролем. Принимаются относительные и абсолютные ссылки (в т.ч. с любым хостом): HTTP-запросы по ним не выполняются, из ссылки берутся только id команды и пароль. Игроку авторизация не нужна: команда идентифицируется паролем из ссылки. Запуск — обычный запуск основного сервиса: ```shell make run ``` MCP-эндпоинт: `http://localhost:8090/api/mcp`. ВНИМАНИЕ: REST-gateway слушает `:8090` (gRPC — `:8080`), а `docker-compose.yml` публикует наружу только `8080` — для доступа к `/api/mcp` извне нужна публикация порта 8090 в compose либо проксирование пути `/api/mcp` через reverse-proxy (nginx/caddy). Эндпоинт публичный (как и REST `/api/teams/{id}/story`); секрет — пароль команды. Подключение к MCP-клиенту (пример для Claude Desktop / аналогов): ```json { "mcpServers": { "evening-detective": { "type": "http", "url": "http://localhost:8090/api/mcp" } } } ``` ## Регистрация команды из Яндекс Форм `POST /api/games/{id}/registrations` — отдельный HTTP webhook, без пользовательского JWT. Капитан не должен иметь учётную запись. Создаётся обычная команда с автоматически сгенерированным паролем; капитану отправляется письмо с данными регистрации, почта капитана и количество участников видны организатору в списке команд игры. Письма автоматически не отправляются. Перед запуском примените миграции и задайте `YANDEX_FORMS_TOKEN` в окружении сервера (случайный секрет, например результат `openssl rand -hex 32`). Пустое значение отключает ручку. Секрет настраивается только в интеграции формы, не в её публичных полях. В Яндекс Формах откройте **Интеграции → Запрос заданным методом**: - Метод: `POST`. - URL: `https://<адрес-сервера>/api/games/42/registrations`, где `42` — ID игры. - Заголовок `Authorization`: `Bearer `. - Заголовок `Content-Type`: `application/json`. - Тело запроса: ```json { "name": "Следствие ведут котики", "email": "captain@example.org", "participantsCount": 5 } ``` Подставьте вместо примеров переменные ответов на вопросы «Название команды», «Почта капитана», «Количество участников». Для строк используйте фильтр JSON, чтобы кавычки и переносы строк корректно экранировались. Число участников передаётся целым JSON-числом без кавычек: от 1 до 2147483647. Название — 1–255 символов после удаления пробелов по краям; email — одиночный адрес длиной до 254 байт. Яндекс передаёт заголовок `X-Delivery-Id`: он обязателен и используется для защиты от повторной доставки. Один ID доставки в пределах игры возвращает ту же команду, не меняя её данные и пароль. Новая отправка формы с другим ID создаёт новую команду, даже если почта и название совпадают. Для ручного теста задайте этот заголовок сами. Успех: `200 {"id":123}`. Пароль команды в ответ webhook не передаётся. Регистрация новой команды доступна только для игры в статусе `draft`. Повтор уже принятой доставки успешен и после старта игры. Ошибки: `400` — поля/JSON/ID доставки; `401` — токен; `409` — игра отсутствует или регистрация закрыта; `503` — интеграция выключена; `500` — внутренняя ошибка. Лимит тела — 16 КиБ, время обработки — до 4 секунд. Сервис должен быть доступен по HTTPS и IPv6: Яндекс Формы отправляют интеграционные запросы только с IPv6. При фильтрации входящего трафика разрешите сеть `2a02:6b8:c00::/40`. Подробности и настройка переменных — в [официальной инструкции Яндекс Форм](https://yandex.ru/support/forms/ru/send-request). Проверка SQL и повторов на отдельной тестовой PostgreSQL: ```sh TEST_DATABASE_URL='postgres://...' go test ./internal/repos/teams_repo -run TestRegistrationPostgres -count=1 ``` Тест создаёт и удаляет собственную схему; требуется право `CREATE SCHEMA`.