generated from VLADIMIR/template
199 lines
15 KiB
Markdown
199 lines
15 KiB
Markdown
# 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 build-builder
|
||
make build-linux
|
||
```
|
||
|
||
По умолчанию используется архитектура Docker-среды. Для другой архитектуры укажите, например, `make build-linux GOARCH=arm64` или `make build-linux GOARCH=amd64`.
|
||
|
||
Миграции
|
||
|
||
```shell
|
||
goose -dir ./migrations create {migration name} sql
|
||
goose -dir ./migrations postgres "user=postgres password=postgres host=localhost dbname=evening_detective_server sslmode=disable" 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 МБ на архив и отдельный файл).
|
||
|
||
## Документация и юридические документы
|
||
|
||
- [`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`).
|
||
|
||
## Агент «Юрист по ПО» (DSH)
|
||
|
||
В каталоге [`agents/software-lawyer`](agents/software-lawyer) лежит агент-пресет для DeepSeek Harness: специализированный консультант по правовым вопросам разработки ПО (лицензии, авторские права, персональные данные 152-ФЗ/GDPR, договоры). Отвечает на основе встроенной базы знаний (`skills/software-law/references/`), без внешних API.
|
||
|
||
Установка (копирует пресет в `~/.dsh/.agent-presets/software-lawyer`):
|
||
|
||
```shell
|
||
./agents/software-lawyer/install.sh
|
||
```
|
||
|
||
После установки пресет «Юрист по ПО» появится в списке агентов при создании новой сессии в веб-интерфейсе DSH. Повторная установка с перезаписью: `./agents/software-lawyer/install.sh --force`.
|
||
|
||
Структура:
|
||
|
||
- `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 <YANDEX_FORMS_TOKEN>`.
|
||
- Заголовок `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`.
|