# Шаринг диалогов Shopping Assistant

Шаринг позволяет создать публичную read-only версию существующего диалога Shopping Assistant. Покупатель может отправить ссылку другому человеку, а клиентский интерфейс — показать сообщения и товарные подборки без доступа к исходному диалогу.

Публичная версия создается как неизменяемый снимок и действует 30 дней. Ее можно отозвать раньше.

!!!primary Важно
`shared_path` ведет к публичному JSON endpoint-у Shopping Assistant API. Если покупателю нужна готовая брендированная страница, реализуйте ее в клиентском канале: получите snapshot по `shared_path` и отобразите сообщения и товары в интерфейсе магазина.
!!!

## Как работает сценарий

1. Покупатель открывает действие «Поделиться» в существующем диалоге.
2. Клиент вызывает `POST /dialogs/share` с идентификаторами пользователя и диалога.
3. Shopping Assistant создает неизменяемый snapshot и возвращает `share_token`, `shared_path` и срок действия.
4. Получатель открывает публичную страницу клиента.
5. Страница вызывает `GET /dialogs/shared/{share_token}` и отображает безопасную версию диалога.
6. При необходимости клиент отзывает ссылку через `POST /dialogs/share/revoke`.

## Идентификаторы

В API шаринга используются snake_case поля. Передавайте в них те же значения, с которыми создавался исходный диалог:

| API шаринга | Shopping Assistant API | Значение |
| :--- | :--- | :--- |
| `user_id` | `resourceId` | `user.uid` пользователя Gravity Field. |
| `dialog_id` | `threadId` | Стабильный ID исходного диалога. |
| `through_message_id` | `assistantMessageId` или `id` из `/shopping/history` | Последнее сообщение, которое нужно включить в публичную версию. |

`tenant_id` не передается в API шаринга. Shopping Assistant проверяет пару `user_id` + `dialog_id` и определяет секцию по сохраненным metadata исходного диалога. Поля `tenant_id`, `client_id` и любые другие поля вне контракта приводят к ответу `400`.

!!!warning Модель доступа
В текущем контракте нет отдельного заголовка авторизации для создания и отзыва ссылки. Совпадение `user_id` + `dialog_id` служит подтверждением доступа к исходному диалогу.

Перед вызовом create или revoke проверяйте в клиентском канале, что текущая сессия относится к указанному `resourceId`. Не раскрывайте пару `resourceId` + `threadId` в публичных URL, сторонней аналитике или общедоступных логах. Передавайте эти идентификаторы в Gravity Field только по правилам [трекинга событий](./tracking.md). Если требования проекта предусматривают отдельную серверную авторизацию, вызывайте API шаринга через backend клиента.
!!!

## `POST /dialogs/share`

Создает новую публичную версию диалога.

```http
POST https://shopping-assistant-api.gravityfield.ai/dialogs/share
Content-Type: application/json
```

### Запрос

```bash
curl --request POST \
  --url 'https://shopping-assistant-api.gravityfield.ai/dialogs/share' \
  --header 'Content-Type: application/json' \
  --data '{
    "user_id": "665f0a000000000000000001",
    "dialog_id": "thread-8f2b1c",
    "through_message_id": "shopping:assistant:8a21..."
  }'
```

| Поле | Обязательное | Назначение |
| :--- | :--- | :--- |
| `user_id` | Да | Значение `resourceId`, с которым был создан диалог. |
| `dialog_id` | Да | Значение `threadId` исходного диалога. |
| `through_message_id` | Нет | Последнее сообщение публичной версии. Сообщение включается в snapshot. Если поле не передано, используется последнее сообщение диалога. |

Можно указать ID сообщения пользователя или ассистента. Сообщение должно принадлежать исходному диалогу.

### Ответ

API возвращает `201 Created`:

```json
{
  "share_token": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
  "shared_path": "/dialogs/shared/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
  "through_message_id": "shopping:assistant:8a21...",
  "expires_at": "2026-09-25T12:00:00.000Z"
}
```

| Поле | Назначение |
| :--- | :--- |
| `share_token` | Токен публичной версии. Считайте его секретом ссылки и не передавайте в аналитику или логи. |
| `shared_path` | Относительный путь публичного JSON endpoint-а. |
| `through_message_id` | Фактическое последнее сообщение, включенное в snapshot. |
| `expires_at` | Дата окончания действия ссылки в ISO 8601. |

Каждый успешный запрос создает отдельный snapshot. Новые сообщения исходного диалога не изменяют уже созданную публичную версию.

## `GET /dialogs/shared/{share_token}`

Возвращает публичную версию диалога. Для запроса не нужны `user_id`, `dialog_id` или `tenant_id`: доступ определяется наличием действующего токена.

```bash
curl --request GET \
  --url 'https://shopping-assistant-api.gravityfield.ai/dialogs/shared/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
```

### Ответ

```json
{
  "created_at": "2026-08-26T12:00:00.000Z",
  "expires_at": "2026-09-25T12:00:00.000Z",
  "messages": [
    {
      "role": "user",
      "created_at": "2026-08-26T11:59:50.000Z",
      "content": "Подбери беговые кроссовки для ежедневных тренировок"
    },
    {
      "role": "assistant",
      "created_at": "2026-08-26T12:00:00.000Z",
      "response": {
        "contents": [
          {
            "type": "message",
            "value": "Вот несколько подходящих вариантов."
          },
          {
            "type": "products",
            "title": "Для ежедневных пробежек",
            "categories": ["Спорт", "Беговые кроссовки"],
            "value": [
              {
                "sku": "SKU-123",
                "category_id": "42",
                "name": "Runner Pro",
                "price": 12990,
                "features": "Амортизация; нейтральная пронация",
                "brand": "Example",
                "url": "https://shop.example/products/SKU-123",
                "image_url": "https://cdn.example/SKU-123.jpg"
              }
            ]
          }
        ]
      }
    }
  ]
}
```

### Контракт публичной версии

```ts
type SharedDialogResponse = {
  created_at: string;
  expires_at: string;
  messages: Array<
    | {
        role: "user";
        created_at: string;
        content: string;
      }
    | {
        role: "assistant";
        created_at: string;
        response: {
          contents: Array<
            | { type: "message"; value: string }
            | {
                type: "products";
                title?: string;
                categories?: string[];
                value: SharedProduct[];
              }
          >;
        };
      }
  >;
};

type SharedProduct = {
  sku?: string;
  category_id?: string;
  name: string;
  price?: number | null;
  features?: string;
  brand?: string;
  url?: string;
  image_url?: string;
};
```

В публичную версию не попадают:

- идентификаторы пользователя, диалога и сообщений;
- изображения, загруженные пользователем;
- кнопки и быстрые ответы;
- оценки ответов;
- latency и tracking events;
- внутренние и служебные поля товарных блоков.

Публичный токен нельзя использовать для продолжения или изменения исходного диалога.

## `POST /dialogs/share/revoke`

Отзывает публичную ссылку.

```bash
curl --request POST \
  --url 'https://shopping-assistant-api.gravityfield.ai/dialogs/share/revoke' \
  --header 'Content-Type: application/json' \
  --data '{
    "user_id": "665f0a000000000000000001",
    "dialog_id": "thread-8f2b1c",
    "share_token": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
  }'
```

API отзывает ссылку только при совпадении `user_id`, `dialog_id` и `share_token`. Ответ `204 No Content` возвращается и после успешного отзыва, и если такая связка не существует. Повторный отзыв безопасен и также возвращает `204`.

После отзыва `GET /dialogs/shared/{share_token}` возвращает `404`.

## Ошибки и ограничения

| Endpoint | HTTP-статус | Причина | Что делать клиенту |
| :--- | :--- | :--- | :--- |
| Create | `400` | Некорректный JSON, отсутствует обязательное поле или передано поле вне контракта. | Проверить payload. Не передавать `tenant_id` и `client_id`. |
| Create | `404` | Диалог не найден, не принадлежит `user_id`, не содержит секцию в metadata или `through_message_id` не найден. | Проверить идентификаторы и при необходимости восстановить историю. |
| Read | `404` | Токен некорректен, не существует, истек или был отозван. | Показать единое состояние «Ссылка недоступна». |
| Любой | `429` | Превышен лимит запросов. | Повторить запрос через число секунд из заголовка `Retry-After`. |
| Любой | `500` | Не удалось создать, получить или отозвать публичную версию. | Показать ошибку и предложить повторить запрос. |

Лимиты API:

- создание и отзыв — до 20 запросов в минуту с одного IP;
- создание — дополнительно до 5 ссылок в минуту для одного диалога;
- получение — до 60 запросов в минуту с одного IP;
- неуспешное получение — до 10 запросов в минуту с одного IP.

Все ответы используют `Cache-Control: no-store`. Не кешируйте snapshot в общем кеше и не передавайте `share_token` в аналитику, error tracking или прикладные логи.

## Рекомендации для интерфейса

- Показывайте действие «Поделиться» только после появления хотя бы одного сообщения в диалоге.
- Если пользователь выбирает конкретный ответ, передавайте его ID как `through_message_id`.
- Для публичной страницы предусмотрите состояния загрузки, недоступной ссылки и ошибки сети.
- Ссылки товаров открывайте по значениям `url` из snapshot-а.
- Не показывайте на публичной странице элементы продолжения диалога, оценки или внутренние действия исходного чата.
- Можно предложить получателю начать новый диалог, но для него нужно создать новые `threadId` и `resourceId`; продолжать исходный диалог по публичному токену нельзя.
- После отзыва ссылки обновляйте состояние интерфейса, не проверяя существование токена по ответу revoke endpoint-а.

## Связанные материалы

[!ref icon="terminal"](./api_reference.md)

[!ref icon="terminal"](./integration.md)
