Разработка и публикация интеграций
Соберите свою интеграцию: определение и бандл, публикация с проверкой sha256, каталог определений в воркспейсе, установка на бота и регистрация вебхука.
Интеграция подключает бота к внешнему сервису. Она даёт канал (входящие и исходящие сообщения), действия и события. Это отдельный TypeScript-проект с тем же контрактом исполнения, что и у бота: декларация плюс бандл. Платформа загружает бандл и запускает его в облаке.
На этой странице — как написать свою интеграцию, а не только поставить готовую из каталога.
Проект состоит из двух частей:
- Определение — файл
integration.definition.ts. Имя, версия, схема конфигурации, каналы, действия, события. Публичный «паспорт» интеграции. - Реализация — код в
src/. Обработчикиregister/unregister, исходящие сообщения каналов и входящийhandlerвебхука.
brt build собирает проект в один самодостаточный бандл .botpress/dist/index.cjs.
Публикация регистрирует определение в каталоге вашего воркспейса и загружает
бандл. После этого интеграцию ставят на ботов — как первопартийную.
Создайте проект
Заведите проект из стартового шаблона:
brt init --type integration --template webhook-message --name my-channel
cd my-channelШаблоны: empty (пустой каркас), hello-world (одно действие),
webhook-message (канал с входящим вебхуком). Минимальный набор файлов:
package.json объявляет имя проекта, скрипты brt и зависимость от SDK. Имя и
версию самой интеграции задаёт определение (name/version в
integration.definition.ts), а не package.json:
{
"name": "my-channel",
"type": "module",
"scripts": {
"build": "brt build",
"publish": "brt integrations publish"
},
"dependencies": {
"@holocronlab/botruntime-sdk": "^6.11.2"
}
}Определение
Опишите интерфейс интеграции в integration.definition.ts. Схему конфигурации
задаёт zod. При публикации она превращается в JSON-схему каталога — по ней
валидируется конфиг на установке.
import { z, IntegrationDefinition } from '@holocronlab/botruntime-sdk'
export default new IntegrationDefinition({
name: 'my-channel',
version: '0.1.0',
configuration: {
schema: z.object({
webhookUrl: z.string().describe('URL, куда постить ответы бота.'),
}),
},
channels: {
webhook: {
messages: {
text: { schema: z.object({ text: z.string() }) },
},
conversation: {
tags: {
id: { title: 'Conversation ID', description: 'ID диалога во внешнем сервисе' },
},
},
},
},
user: {
tags: {
id: { title: 'User ID', description: 'ID пользователя во внешнем сервисе' },
},
},
})Ключевые блоки:
| Блок | Назначение |
|---|---|
configuration.schema | поля установки (URL, идентификаторы); секретные значения запечатываются при установке |
channels | каналы диалога: какие типы сообщений интеграция принимает и отправляет |
actions | вызываемые операции (input/output через zod) — доступны боту как инструменты |
events | именованные события, которые интеграция эмитит внутрь платформы |
user.tags / conversation.tags | внешние идентификаторы, по которым платформа склеивает диалоги и пользователей |
Параллельное выполнение
По умолчанию платформа выполняет вызовы одной интеграции строго по одному. Это
сохраняет совместимость с обработчиками, которые не рассчитаны на параллельный
доступ. Если действия интеграции независимы, разрешите до четырёх одновременных
вызовов через maxConcurrency:
export default new IntegrationDefinition({
// ...
maxConcurrency: 4,
})maxConcurrency — целое число от 1 до 4; отсутствие поля означает 1.
Платформа лениво поднимает до указанного числа отдельных process slots. Поэтому
падение одного процесса не останавливает уже работающие соседние slots, а
остальные вызовы ждут в ограниченной FIFO-очереди. Например, при десяти
одновременных вызовах и maxConcurrency: 4 одновременно исполняются не более
четырёх.
Включайте параллельность только для reentrant-обработчиков:
- не храните состояние запроса в изменяемых глобальных переменных;
- не полагайтесь на порядок завершения параллельных действий;
- делайте внешние side effects идемпотентными или передавайте провайдеру уникальный ключ операции;
- учитывайте, что
maxExecutionTimeограничивает сам handler после запуска; очередь и запуск process slot имеют отдельные ограниченные бюджеты, а все фазы вместе должны уложиться во внешний дедлайн вызова бота.
Egress / сеть
Интеграция объявляет исходящие адреса блоком network в определении: какие
хосты она вызывает снаружи и доставляется ли вебхук через relay-базу шлюза.
Декларация уезжает на сервер при публикации и формирует allowlist сетевого
шлюза (RKN-контур) — без неё платформа не знает, куда интеграции разрешено
ходить.
export default new IntegrationDefinition({
// ...
network: {
providerHosts: ['api.telegram.org'],
ingressRelayed: true,
},
})| Поле | Назначение |
|---|---|
providerHosts | хосты, к которым интеграция обращается наружу; пустой список — осознанное «эта интеграция не делает внешних вызовов» |
ingressRelayed | доставлять входящий вебхук через relay-базу шлюза, а не на основной публичный URL |
webhookAuthMode | режим проверки входящего вебхука — разбор ниже, в разделе про установку |
Динамический per-install хост (поддомен из конфига установки, как у
провайдеров с аккаунтом в адресе) описывайте wildcard-доменом
(*.example.com). Полностью произвольный URL, неизвестный до установки,
текущая модель не поддерживает — не публикуйте такую интеграцию молча.
Первая публикация версии обязана нести providerHosts — POST/PUT без
этого поля сервер отклоняет с 400. Явный пустой список
(providerHosts: []) — легальное «наружу не хожу», отличное от «забыл
объявить». Причина жёсткости: раньше отсутствующее поле молча превращалось в
пустой allowlist — интеграция публиковалась «успешно», а в проде каждый её
вызов провайдера гас на сетевом шлюзе без единого предупреждения на этапе
публикации.
Повторная публикация той же версии (обновление байтов бандла или метаданных
через PUT) может не повторять providerHosts в теле — сервер наследует уже
сохранённую декларацию. Требование распространяется только на первую
публикацию версии, а не на каждый редеплой.
Реализация
Реализация — это new Integration({...}) в src/index.ts. register и
unregister вызываются при сохранении и снятии конфигурации: в них проверяют
доступы и заводят или убирают ресурсы во внешнем сервисе. Исходящие сообщения
канала уходят наружу. Входящий handler принимает вебхук и заводит сообщение в
диалог.
import * as sdk from '@holocronlab/botruntime-sdk'
import axios from 'axios'
export default new sdk.Integration({
register: async ({ ctx }) => {
// Проверьте конфигурацию и доступ к внешнему сервису.
if (!ctx.configuration.webhookUrl) {
throw new sdk.RuntimeError('webhookUrl обязателен')
}
},
unregister: async () => {},
actions: {},
channels: {
webhook: {
messages: {
text: async ({ ctx, conversation, payload }) => {
await axios.post(ctx.configuration.webhookUrl, {
conversationId: conversation.tags.id,
text: payload.text,
})
},
},
},
},
handler: async ({ client, req }) => {
if (!req.body) {
return { status: 400, body: JSON.stringify({ error: 'No body' }) }
}
const parsed = sdk.z
.object({ userId: sdk.z.string(), conversationId: sdk.z.string(), text: sdk.z.string() })
.safeParse(JSON.parse(req.body))
if (!parsed.success) {
return { status: 400, body: JSON.stringify({ error: 'Invalid body' }) }
}
const { userId, conversationId, text } = parsed.data
const { conversation } = await client.getOrCreateConversation({
channel: 'webhook',
tags: { id: conversationId },
})
const { user } = await client.getOrCreateUser({ tags: { id: userId } })
await client.createMessage({
type: 'text',
conversationId: conversation.id,
userId: user.id,
payload: { text },
tags: {},
})
return { status: 200, body: JSON.stringify({ ok: true }) }
},
})Долгие действия и большие файлы
POST /v1/chat/integration-operations хранит состояние операции, обеспечивает
идемпотентный запуск и исполняет её в отдельной ограниченной очереди. Сам
endpoint не делает существующий action долгоживущим: поддержка зависит
от конкретного action и его адаптера.
Сейчас универсальный fallback вызывает один обычный integration action. На него
по-прежнему действует maxExecutionTime из определения интеграции — не более
119 секунд. Fallback не умеет отдельно запустить работу у провайдера, опрашивать
её состояние или отменять её через API провайдера.
Запустите операцию через POST /v1/chat/integration-operations. Передайте
стабильный Idempotency-Key: повтор того же запроса вернёт прежний
operationId, а другой запрос с тем же ключом получит 409.
Если две операции меняют один и тот же ресурс провайдера, передайте
resourceKey — стабильный идентификатор назначения или пути длиной до
255 байт. В пределах одной установленной версии интеграции одинаковый ключ
разрешает только одну активную операцию. Разные ключи и запросы без ключа
сохраняют обычный ограниченный параллелизм.
curl -sS https://botruntime.ru/v1/chat/integration-operations \
-H "Authorization: Bearer $BRT_TOKEN" \
-H "x-bot-id: $BOT_ID" \
-H "Idempotency-Key: catalog-refresh-42" \
-H "Content-Type: application/json" \
-d '{
"type": "my-channel:refreshCatalog",
"input": {
"catalogId": "catalog_42"
},
"resourceKey": "catalog:catalog_42",
"timeoutSeconds": 90
}'Запрос хранит только управляющие данные: всё тело ограничено 68 КиБ, поле
input — 64 КиБ. Поле с именем contentBase64 запрещено на любой глубине.
Для обычного действия input остаётся непрозрачным JSON.
Длительное действие может явно включить файловый контракт версии 1. Для этого
его схема помечает узлы расширением x-botruntime-fileRef и выбирает режим:
resolve-currentпринимает{id}и закрепляет текущее поколение в момент запуска;exactпринимает{id, generation}и закрепляет именно это поколение.
Путь и количество ссылок задаёт схема конкретного действия. Платформа
поддерживает вложенные объекты и массивы, но принимает не более 32 файловых
ссылок на операцию и не более 1 ГиБ на один файл. Сервер проверяет область
рабочей области и бота, заменяет селектор полной авторитетной ссылкой
{version,id,generation,checksum,size,contentType,filename} и сохраняет
закрепление до запуска обработчика.
Обработчик не получает файловые адреса или полный токен бота. SDK выдаёт ему
потоковый клиент только при наличии разрешения files:"1" в очищенном
конверте попытки. Клиент умеет читать весь закреплённый файл или диапазон,
проверяет размер, тип и контрольную сумму и не загружает файл целиком в память.
Действие также может включить защищённую контрольную точку. SDK выдаёт клиент
checkpoint только при разрешении checkpoint:"1". Запись требует ожидаемую
редакцию, только дополняет подтверждённые факты и отклоняет истёкшую попытку.
Обычное состояние интеграции для такого журнала не подходит: оно не связано
с арендой операции.
Читайте состояние через
GET /v1/chat/integration-operations/{operationId}. Возможные состояния:
| Состояние | Что означает |
|---|---|
queued | операция ждёт места в ограниченной очереди |
running | выполняется действие или попытка адаптера поставщика |
succeeded | результат сохранён в result |
failed | получен определённый отказ |
cancel_requested | локальная отмена запрошена, исход работы у провайдера ещё не известен |
cancelled | серверный адаптер подтвердил отмену |
outcome_unknown | запрос мог сработать у провайдера, но ответ потерян |
abandoned | оператор остановил дальнейшую сверку, не объявляя внешний эффект успешным, неуспешным или отменённым |
outcome_unknown нельзя повторять вслепую. Адаптер поставщика должен сначала
найти исходную операцию по стабильному идентификатору. Только доказанный ответ
«эффекта нет» разрешает новую попытку. Универсальный резервный обработчик такой сверки
не делает: операция остаётся в outcome_unknown, автоматическая обработка
прекращается по deadline, а дальше нужно ручное решение.
Операция в outcome_unknown продолжает резервировать свой resourceKey: её
внешний эффект ещё может выполняться. Worker переоткладывает следующий запрос
с тем же ключом до reconcile, доказанного безопасного повтора, терминального
результата или явного операторского решения. Конфликт не запускает обработчик
интеграции и не увеличивает attempt.
После проверки у провайдера owner или admin может явно завершить неразрешимую
операцию через
POST /v1/admin/workspaces/{workspaceId}/bots/{botId}/integration-operations/{operationId}/abandon
с обязательной причиной. Переход разрешён только из outcome_unknown.
abandoned освобождает инсталляцию для реконфигурации или удаления, но не
доказывает, что внешний эффект отсутствует. Актор, причина, время и исходный
статус сохраняются атомарно в журнале аудита. Если reconcile/cancel ещё
удерживает действующую аренду, метод возвращает
409 OPERATION_CONTROL_IN_PROGRESS; после завершения попытки или истечения
аренды оператор может повторить тот же запрос.
POST /v1/chat/integration-operations/{operationId}/cancel запрашивает отмену.
Операция в очереди отменяется сразу. Запущенная попытка получает локальный
сигнал отмены, но универсальный резервный обработчик не отменяет внешнюю работу
через API поставщика. Конечное состояние cancelled требует подтверждения
серверного адаптера.
Метод устойчивой операции не делает один JavaScript-обработчик бессрочным. Интеграция,
которая работает дольше 119 секунд, должна получить отдельный адаптер
Start/Poll/Cancel с идемпотентной сверкой результата у провайдера.
Для Яндекс Диска такой адаптер и переход с base64 на проверенный fileRef ещё
нужны; до их выпуска multi-minute upload и fileRef-путь не считаются
поддержанными.
Соберите бандл
brt build упаковывает проект в один самодостаточный файл. SDK, клиент API и
раннер лежат внутри — платформе не нужны зависимости проекта:
brt build
# → .botpress/dist/index.cjsБандл экспортирует default (реализацию) и handler (входящий вебхук). Именно
этот файл читает публикация. Платформа исполняет его в отдельном процессе.
Опубликуйте
brt integrations publish публикует integration-проект через тот же
Botpress-shaped deploy-контракт, что и brt deploy. Метаданные Hub, actions,
channels, events, attributes, network и bundle принадлежат одной integration-
сущности и не могут попасть в разные каталожные записи. Бот для публикации не
нужен: команда работает под profile PAT в скоупе воркспейса.
brt integrations publish --dryRun
brt integrations publishПо шагам команда:
- читает полный
integration.definition.tsи собирает bundle; - добавляет handle воркспейса к имени:
yookassaстановитсяbotruntime/yookassa; - ищет свою версию в owned-реестре, а публичные зависимости — в Hub;
- создаёт или обновляет одну integration-сущность с
visibility=public.
--dryRun вызывает те же серверные проверки через validate endpoint, но не
пишет определение и bundle. Для приватной версии используйте обычный
brt deploy: его visibility по умолчанию — private. Публичная публикация
доступна платформенному воркспейсу; другой workspace получает явную ошибку, а
не скрытый private deploy.
Флаги:
| Флаг | Назначение |
|---|---|
--noBuild | не пересобирать — использовать уже собранный .botpress/dist/index.cjs |
--dryRun | проверить create/update без записи |
--api-url <url> | выбрать прокси или стенд для разработки платформы |
Команда работает только из integration-проекта. Флаги --name,
--version-number, --config-schema-file и --noBundle удалены: они обходили
полное определение и позволяли опубликовать схему без метаданных или кода.
icon должен ссылаться на SVG — это тот же контракт, который проверяет
Botpress CLI. PNG/ICO сначала преобразуйте или заверните в SVG-контейнер.
Owned-реестр и публичный Hub
Owned-реестр содержит только интеграции текущего воркспейса. Публичные версии других владельцев читаются через Hub. Такое разделение не даёт CLI принять чужую публичную карточку за свою и обновить неверный ID.
| Метод и путь | Действие |
|---|---|
GET /v1/admin/integrations | список интеграций текущего воркспейса |
POST /v1/admin/integrations | создать полное определение и bundle |
PUT /v1/admin/integrations/{id} | обновить свою версию |
POST /v1/admin/integrations/validate | проверить создание без записи |
PUT /v1/admin/integrations/{id}/validate | проверить обновление без записи |
DELETE /v1/admin/integrations/{id} | удалить свою версию; установка блокирует удаление с 409 |
GET /v1/admin/hub/integrations | список публичных интеграций всех воркспейсов |
GET /v1/admin/hub/integrations/{name}/{version} | получить публичную версию и её ownerWorkspace |
Режимы inbound-аутентификации не взаимозаменяемы:
shared_secret— kernel проверяет общий секрет до запуска бандла;provider_verified— для провайдера без подписи: platform-owned handler повторно читает объект через API провайдера до эмита события;handler_verified— для first-party HTTP API со своей per-user auth на каждом route (Chat используетx-user-key/HS256 JWT). Kernel по-прежнему резолвит webhook и tenant, но не требует общего webhook secret.
handler_verified нельзя использовать как короткий путь для внешнего webhook:
обычная пользовательская интеграция должна выбрать shared_secret.
Установите на бота
Определение из каталога ставится на конкретного бота. Публикация адресовала воркспейс — установка адресует production target из project link. Команда аутентифицируется PAT активного профиля и требует роль owner/admin; per-bot key для установки и регистрации не нужен.
В исходном IntegrationDefinition имя остаётся одним локальным сегментом.
Публичный каталог добавляет handle публикующего воркспейса: исходное
name: 'yookassa' устанавливается как botruntime/yookassa@0.1.0. Bare-ref
name@version остаётся допустимым для приватных определений.
printf '{"webhookUrl":"https://example.com/hook"}' \
| brt integrations install my-channel@0.1.0 --config-stdin
export WEBHOOK_ID='wh_replace_from_install_output'
brt integrations register "$WEBHOOK_ID"install возвращает публичный webhookId. register создаёт и передаёт
провайдеру необходимый webhook secret внутри платформенного dispatcher; сырой
секрет не требуется локальному проекту. Входящий трафик платформа проверяет по
сохранённому значению.
У встроенных провайдеров без webhook-подписи определение может использовать
webhookAuthMode: "provider_verified". Такой режим доступен только
platform-owned каталогу: секрет не создаётся, а бандл обязан повторно запросить
объект через API провайдера и сверить статус и значимые поля до эмита события.
Одного доверия к телу webhook недостаточно.
Режим авторизации фиксируется при создании name@version. Его нельзя сменить
редеплоем той же версии: сервер отклонит запрос до публикации бандла. Repoint
инсталляции между версиями с разными режимами также запрещён — новую версию
нужно установить заново, чтобы секрет и проверяющий код не разошлись.
Для agent-проекта install/register меняют состояние в Cloud и обновляют
dependency snapshot, но не собирают и не загружают bot bundle. После регистрации выполните
brt deploy --adk и только затем проверяйте production-канал. Для fresh agent
target канонический порядок начинается с первого deploy, а не с ручного link:
deploy → install/register → второй deploy → приёмочная проверка.
Секреты никогда не идут через аргументы командной строки — только через stdin или
--config-file/--value-file. В режиме shared_secret webhookSecret
выдаётся один раз при установке.
Потеряли — снимите установку и поставьте заново.
Эндпоинты установки адресуют бота — заголовок x-bot-id:
| Метод и путь | Действие |
|---|---|
POST /v1/admin/integrations/install | поставить интеграцию на бота; тело {name, version, alias?, config} → {installationId, webhookId, webhookSecret} |
POST /v1/admin/integrations/{webhookId}/register | зарегистрировать вебхук установки → {ok, webhookId, webhookUrl} |
Реестр интеграций
Owned-реестр возвращает только интеграции текущего воркспейса. Публичные версии читаются через Hub:
curl -sS https://botruntime.ru/v1/admin/integrations/my-channel/0.1.0 \
-H "Authorization: Bearer $BRT_TOKEN"| Метод и путь | Действие |
|---|---|
GET /v1/admin/integrations | список своих интеграций |
GET /v1/admin/integrations/{name}/{version} | получить свою интеграцию по имени и версии |
GET /v1/admin/integrations/{id} | получить свою интеграцию по идентификатору |
GET /v1/admin/hub/integrations/{name}/{version} | получить публичную интеграцию |
Те же чтения есть в CLI: brt integrations get <ref>, brt integrations list,
brt integrations delete <ref>.