АПИ AI-чата: диалоги, сообщения и файлы по ключу СУДиДЕЛО

Методы AiChat позволяют встроить AI-чат СУДиДЕЛО во внешнее приложение: создать отдельный диалог, отправлять сообщения, получать полный ответ или поток событий и скачивать сформированные файлы. Для подключения используется существующий пользовательский ключ СУДиДЕЛО в заголовке X-API-Key.

Что нужно для подключения

Базовый адрес — https://api-sudodelo.torkndgov.ru. Актуальную доступность проверяйте в Swagger PublicApi, группа AiChat. Как получить обычный пользовательский ключ, описано в общей справке по открытому API.

  • В каждом запросе передавайте X-API-Key: <ваш пользовательский ключ>. Используйте HTTPS. Ключ не должен попадать в URL, скриншоты, журналы или общедоступный код.
  • Сотрудник должен быть активным, принадлежать действующей организации и иметь право AI-чат (menu.aichat). Проверки выполняются заново при каждом запросе. Отозванный или заменённый ключ больше не предоставляет доступ.
  • Внешнему приложению не нужны логин, пароль, cookie WebApp, launch-token, Bearer Python-чата или общий серверный секрет. Их не следует добавлять к запросам этих методов.
  • Передавайте ровно один X-API-Key. Несколько значений, смешение с query-параметром apiKey либо заголовком Authorization отклоняются. Cookie не используются как credential.
  • AccountId и OrganizationId клиент не выбирает. Сервер определяет сотрудника по проверенному ключу, а доступ к диалогу проверяет по сотруднику, а не только по организации.

Последовательность работы

  • Создать диалог с одним X-API-Key и сохранить ИдентификаторДиалога.
  • Отправлять этот идентификатор и тот же действующий ключ при запросе состояния, сообщениях JSON/SSE и скачивании файлов.
  • Для продолжения переписки использовать прежний идентификатор. Для новой независимой переписки явно создать новый диалог.

ИдентификаторДиалога — несекретная строка из 32 строчных шестнадцатеричных символов. Она не является токеном доступа: без действующего ключа владельца диалог недоступен. У одного сотрудника может быть несколько диалогов, каждый со своей историей и файлами.

1. Создать диалог

POST /api/AiChat/sessions
X-API-Key: <пользовательский apiKey>

Тело запроса отсутствует. Не отправляйте даже пустой JSON {}, launch-token или идентификаторы сотрудника. Сервер сам создаёт и обменивает одноразовый токен, а секрет доступа к Python-чату хранит у себя.

curl --fail-with-body --request POST \
  "https://api-sudodelo.torkndgov.ru/api/AiChat/sessions" \
  --header "X-API-Key: $SUDODELO_API_KEY"

Ответ 201 Created, Cache-Control: no-store:

{
  "ИдентификаторДиалога": "0123456789abcdef0123456789abcdef",
  "ВремяНеактивностиВСекундах": 28800
}

Идентификатор и срок в примере условные. Используйте значения фактического ответа; срок приходит от чат-сервиса. В ответе нет apiKey, launch-token, Bearer и create-secret.

  • ИдентификаторДиалога — Несекретный идентификатор независимого диалога. Сам по себе не предоставляет доступ: требуется X-API-Key владельца.
  • ВремяНеактивностиВСекундах — Скользящий срок сессии в секундах; авторизованные запросы продлевают его.

2. Получить сведения о диалоге

GET /api/AiChat/session?ИдентификаторДиалога=<идентификатор>
X-API-Key: <пользовательский apiKey>

Обязательный query-параметр ИдентификаторДиалога берётся из создания диалога. Пустой или неверно оформленный параметр отклоняется; неизвестный или чужой корректный идентификатор возвращает 404.

curl --fail-with-body --get \
  "https://api-sudodelo.torkndgov.ru/api/AiChat/session" \
  --header "X-API-Key: $SUDODELO_API_KEY" \
  --data-urlencode "ИдентификаторДиалога=$DIALOG_ID"

Ответ 200 OK:

{
  "Авторизован": true,
  "Модель": "<модель из конфигурации>",
  "НазваниеМодели": "<название для отображения>",
  "ВремяНеактивностиВСекундах": 28800
}
  • Авторизован — Признак действующей авторизации диалога.
  • Модель — Идентификатор модели, настроенной на сервере чата.
  • НазваниеМодели — Название модели для отображения.
  • ВремяНеактивностиВСекундах — Скользящий срок сессии в секундах; авторизованные запросы продлевают его.

Авторизованные обращения поддерживают скользящий срок неактивности. Фиксированная дата истечения не возвращается. Регулярный запрос состояния также является обращением к сессии; он не восстанавливает уже истёкший диалог.

3. Отправить сообщение: JSON или SSE

POST /api/AiChat/chat
X-API-Key: <пользовательский apiKey>
Content-Type: application/json
{
  "ИдентификаторДиалога": "0123456789abcdef0123456789abcdef",
  "Сообщение": "Какие события есть в моём календаре?",
  "ПотоковыйОтвет": false
}
  • ИдентификаторДиалога — Несекретный идентификатор диалога из ответа создания; доступ требует действующего ключа владельца.
  • Сообщение — Текст от 1 до 16000 символов; внешние пробелы удаляются, пустой текст запрещён.
  • ПотоковыйОтвет — Истина — поток SSE, ложь (по умолчанию) — полный JSON-ответ. Только JSON boolean.

Укажите сообщение от 1 до 16000 символов. Внешние пробелы удаляются; пустой после удаления текст запрещён. ПотоковыйОтвет — настоящий JSON boolean (true/false), по умолчанию false. Лишние поля запрещены: клиент не присылает историю, системный промпт, модель, инструменты или AccountId.

При ПотоковыйОтвет: false возвращается 200 application/json:

{
  "Текст": "В календаре событий нет.",
  "Файлы": [],
  "События": [
    {
      "Тип": "Текст",
      "Текст": "В календаре событий нет."
    },
    {
      "Тип": "Завершение"
    }
  ]
}
  • Текст — Проверенный текст ответа ассистента.
  • Файлы — Файлы, полученные в том же ходе диалога.
  • События — Упорядоченные события одного запуска; последнее событие успешного ответа — Завершение.

Содержимое и число событий зависят от запроса. Текст — проверенный ответ того же запуска чата, а не дополнительное резюме обёртки. Для следующего сообщения используйте тот же идентификатор: так сохраняется история диалога.

Как читать поток событий

При ПотоковыйОтвет: true ответ имеет тип text/event-stream, заголовки Cache-Control: no-store и X-Accel-Buffering: no. Каждая запись начинается с data: , затем следует JSON и пустая строка. Например:

data: {"Тип":"Текст","Текст":"В календаре событий нет."}

data: {"Тип":"Завершение"}

  • СлужебнаяИнформация: поля КоличествоИнструментов, СессияИнструментов.
  • ВызовИнструмента: ИмяИнструмента, АргументыИнструмента. Аргументы — JSON-строка: исходные имена сохраняются внутри значения, а не становятся полями внешнего контракта.
  • РезультатИнструмента: ИмяИнструмента, ПредварительныйРезультат.
  • Файл: вложенный объект Файл с метаданными скачивания.
  • Текст: поле Текст ответа ассистента.
  • Завершение: подтверждение успешного окончания хода.
  • Ошибка: поле Код, например turn_failed. После ошибки событие «Завершение» не выдаётся.

Успех SSE подтверждает только событие «Завершение». После начала потока HTTP-код уже не меняется: сбой сообщается событием «Ошибка». Закрытие соединения без «Завершение» означает неизвестный исход. Отказ grounding, оформленный самим чатом как ответ ассистента, может быть обычным успешным ответом; HTTP-успех не определяют по формулировке текста.

4. Скачать файл диалога

GET /api/AiChat/files/{ИдентификаторФайла}?ИдентификаторДиалога=<идентификатор>
X-API-Key: <пользовательский apiKey>

Параметр маршрута ИдентификаторФайла берётся из массива Файлы либо события Файл. Допустимы латинские буквы, цифры, дефис и подчёркивание, длина — до 256 символов. Обязательный query-параметр ИдентификаторДиалога указывает диалог, в котором сформирован файл.

  • ИдентификаторФайла — Идентификатор файла, доступного только владельцу диалога.
  • Имя — Исходное имя файла.
  • ТипСодержимого — Тип содержимого файла (MIME).
  • РазмерВБайтах — Размер файла в байтах.
  • АдресСкачивания — Локальный путь через обёртку с идентификатором диалога; требует X-API-Key владельца.
  • Вид — Категория файла из сервиса чата.

АдресСкачивания — путь PublicApi с несекретным идентификатором диалога в query; в нём нет ключа или Bearer. Соединяйте такой путь с origin PublicApi. Даже готовую ссылку нужно открывать HTTP-запросом с X-API-Key; обычный переход браузера заголовок не добавляет.

curl --fail-with-body --get \
  "https://api-sudodelo.torkndgov.ru/api/AiChat/files/$FILE_ID" \
  --header "X-API-Key: $SUDODELO_API_KEY" \
  --data-urlencode "ИдентификаторДиалога=$DIALOG_ID" \
  --output report.xlsx

Ответ передаётся потоком с исходными Content-Type, Content-Disposition и именем файла, включая RFC 5987. Изображения могут выдаваться inline, другие файлы — attachment. Неизвестный файл или файл другого диалога возвращает 404, в том числе между двумя диалогами одного сотрудника. Собственный истёкший файл возвращает 410 file_expired, пока запись различима в хранилище; после очистки возможен 404. Скачивание не продлевает срок хранения файла.

Пример внешнего клиента: весь сценарий

Bash, curl и jq. Перед запуском задайте SUDODELO_API_KEY в окружении, не сохраняйте его в скрипте и не включайте set -x. Пример сначала получает JSON-ответ и файлы, затем отправляет SSE-сообщение в тот же диалог. Первый вопрос можно задать через SUDODELO_CHAT_MESSAGE; файлы будут скачаны, если чат их сформировал.

set -euo pipefail
: "${SUDODELO_API_KEY:?Задайте API-ключ в окружении}"
BASE="https://api-sudodelo.torkndgov.ru"
MESSAGE="${SUDODELO_CHAT_MESSAGE:-Привет!}"

created=$(curl --fail-with-body --silent --show-error \
  --request POST "$BASE/api/AiChat/sessions" \
  --header "X-API-Key: $SUDODELO_API_KEY")
DIALOG_ID=$(printf '%s' "$created" | jq -er '."ИдентификаторДиалога"')

curl --fail-with-body --silent --show-error --get \
  "$BASE/api/AiChat/session" \
  --header "X-API-Key: $SUDODELO_API_KEY" \
  --data-urlencode "ИдентификаторДиалога=$DIALOG_ID" >/dev/null

reply=$(jq -n --arg id "$DIALOG_ID" --arg message "$MESSAGE" \
  '{"ИдентификаторДиалога":$id,"Сообщение":$message,"ПотоковыйОтвет":false}' | \
  curl --fail-with-body --silent --show-error --max-time 950 \
    "$BASE/api/AiChat/chat" \
    --header "X-API-Key: $SUDODELO_API_KEY" \
    --header "Content-Type: application/json" --data-binary @-)
printf '%s' "$reply" | jq -er '."События"[-1]."Тип" == "Завершение"' >/dev/null
printf '%s' "$reply" | jq -r '."Текст"'

# Идентификатор файла берём только из ответа. Скачиваем поток в отдельный файл.
while IFS= read -r FILE_ID; do
  [[ "$FILE_ID" =~ ^[A-Za-z0-9_-]{1,256}$ ]] || exit 1
  curl --fail-with-body --silent --show-error --get \
    "$BASE/api/AiChat/files/$FILE_ID" \
    --header "X-API-Key: $SUDODELO_API_KEY" \
    --data-urlencode "ИдентификаторДиалога=$DIALOG_ID" \
    --output "download-$FILE_ID.bin"
done < <(printf '%s' "$reply" | jq -r '."Файлы"[]."ИдентификаторФайла"')

# Читайте события до «Завершение»; один HTTP 200 не подтверждает успех SSE.
jq -n --arg id "$DIALOG_ID" \
  '{"ИдентификаторДиалога":$id,"Сообщение":"Повтори мой предыдущий вопрос.","ПотоковыйОтвет":true}' | \
  curl --fail-with-body --silent --show-error --no-buffer --max-time 950 \
    "$BASE/api/AiChat/chat" \
    --header "X-API-Key: $SUDODELO_API_KEY" \
    --header "Content-Type: application/json" --data-binary @-

Пример не повторяет POST автоматически. Для прикладного клиента реализуйте разбор SSE, проверку терминального события и обработку ошибок по следующему разделу. Не записывайте ответы с персональными данными в общедоступные журналы.

Ошибки и повторные запросы

{
  "Код": "session_expired",
  "Сообщение": "Сессия диалога истекла. Создайте новый диалог явно."
}
  • 401 invalid_api_key — ключ отсутствует, неоднозначен, неверен, отозван; сотрудник неактивен или организация недоступна.
  • 403 permission_denied — проверенному сотруднику не предоставлено право menu.aichat.
  • 404 dialogue_not_found — диалог отсутствует или принадлежит другому сотруднику.
  • 410 session_expired — срок диалога истёк либо Python-сессия больше не действует. Создайте новый диалог явным действием клиента.
  • 404 not_found, 410 file_expired — файл неизвестен/чужой либо истёк.
  • 422 validation_error — неверные параметры, лишнее поле, неподходящий тип или тело создания диалога.
  • 502 invalid_upstream_response — некорректный ответ чат-сервиса; 502 turn_failed — ход не завершён успешно.
  • 503 upstream_unavailable — backend недоступен или интеграция не настроена; 503 dialogue_capacity — достигнут серверный предел записей диалогов.
  • 504 upstream_timeout — истекло время ожидания backend.

Все ответы с данными и ошибками содержат Cache-Control: no-store. Ошибки не возвращают секреты и внутренние исключения. Не повторяйте автоматически POST создания диалога и сообщения: одноразовый токен мог уже быть погашен, а MCP-инструменты могли выполнить изменение CRM до сетевого обрыва. Отмена запроса останавливает дальнейшее чтение, но уже выполненные действия не откатываются. Тайм-аут клиента и reverse proxy должен учитывать несколько раундов работы модели и инструментов; стандартный серверный тайм-аут хода — 900 секунд.

История, срок жизни и перезапуски

Один идентификатор связан с одним диалогом и владельцем. История не объединяется автоматически с открытой web-вкладкой или другими диалогами сотрудника. Сервер проверяет владельца до срока и обращения к Python API.

Соответствие идентификатора и внутреннего токена хранится в памяти одного процесса PublicApi. После его перезапуска диалог недоступен, клиент должен явно создать новый. После перезапуска Python-чата сохранённая запись PublicApi не восстанавливает утраченную сессию и возвращает контролируемое истечение. Несколько экземпляров PublicApi требуют привязки диалога к одному экземпляру либо общего защищённого хранилища; архитектуру нескольких Python workers также нужно согласовать отдельно.

Методы не добавляют постоянный список диалогов, удаление диалога, загрузку вложений пользователя или возобновление оборванного SSE. Существующий запуск AI-чата из WebApp сохраняется. Подключение произвольного браузерного frontend с другого origin требует отдельно согласованной CORS-политики.

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

Открытое API системы СУДиДЕЛО · Swagger PublicApi: AiChat.