Files
evening_detective_server/README.md
T
2026-09-13 01:42:22 +07:00

190 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
Запуск
```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`.