Адаптер платформы — это мост между сырым JSON/XML запросом от внешней платформы и унифицированным контроллером BotController. Ваша задача: распарсить входящие данные, наполнить контроллер, обработать UI-компоненты (кнопки, картинки, звуки) и сформировать ответ строго по контракту конкретной платформы.
Адаптер наследуется от базового класса BasePlatformumbot/plugins).
Когда на сервер приходит запрос, фреймворк перебирает все подключенные адаптеры и спрашивает: «Это твой запрос?».
Вы должны реализовать метод, который по заголовкам или телу запроса понимает, относится ли он к вашей платформе.
Пример: Платформа WeChat отправляет специфичный заголовок x-wechat-signature и XML в теле. Telegram отправляет заголовок x-telegram-bot-api-secret-token.
isPlatformOnQuery(query: any, headers?: Record<string, unknown>): boolean {
// 1. Проверяем заголовки (самый надежный способ)
if (headers?.['x-wechat-signature']) return true;
// 2. Фоллбэк: проверяем уникальные поля в теле запроса
return !!(query.xml_msg || query.specific_wechat_field);
}
Если платформа требует проверки подписи (токена), переопределяйте этот метод. По умолчанию BasePlatform уже умеет проверять HMAC SHA256, если вы укажете signatureName в классе адаптера.
Если стандартной проверки недостаточно (например, платформа использует Ed25519 вместо HMAC SHA256), переопределите метод isCorrectQuery и реализуйте свою логику валидации
Задача: Взять сырой query и заполнить поля controller. От того, как вы заполните контроллер, зависит корректная работа бизнес-логики приложения
Обязательные поля для заполнения:
controller.userId (string | number) — уникальный ID пользователя.controller.userCommand (string) — текст команды в нижнем регистре (нужно для поиска команд).controller.originalUserCommand (string) — оригинальный текст как есть.controller.messageId (number | string) — ID сообщения (нужно для определения начала диалога).Опциональные, но важные поля:
controller.nlu.setNlu(...) — если платформа присылает NLU/интенты.controller.userMeta — метаданные (например, есть ли у юзера экран).controller.payload — дополнительные данные (например, нажатая кнопка).setQueryData(query: any, controller: BotController): boolean {
if (!query) {
controller.platformOptions.error = 'Пустой запрос';
return false;
}
controller.requestObject = query; // Сохраняем оригинал
controller.userId = query.user_id;
controller.userCommand = (query.text || '').toLowerCase().trim();
controller.originalUserCommand = query.text || '';
controller.messageId = query.message_id;
// Если платформа присылает данные о юзере
if (query.user) {
controller.nlu.setNlu({ thisUser: { username: query.user.name } });
}
return true;
}
Фреймворк оперирует абстракциями (IButtonType, ICardInfo). Платформы требуют специфичные форматы. Чтобы превратить абстракцию в формат платформы, используются функции-процессоры.
Вам нужно написать функцию, которая принимает массив абстрактных кнопок и возвращает объект, понятный платформе.
Метод controller.buttons.getButtons(ваш_процессор) сам вызовет вашу функцию и отдаст результат.
// 1. Пишем процессор
function myPlatformButtonProcessing(buttons: IButtonType[]): MyPlatformKeyboard {
return {
inline_keyboard: buttons.map((btn) => ({
text: btn.title,
callback_data: btn.payload ? JSON.stringify(btn.payload) : btn.title,
})),
};
}
// 2. Вызываем внутри getContent
const keyboard = controller.buttons.getButtons(myPlatformButtonProcessing);
Важно: Платформы не принимают локальные пути к файлам (/img/pic.jpg). Им нужны token или url, загруженный на их серверы.
Фреймворк предоставляет утилиту getImageToken. Она проверяет БД: если токен для этой картинки уже есть — возвращает его. Если нет — вызывает ваш callback, где вы сами загружаете картинку в API платформы и сохраняете токен в БД.
import { getImageToken, ImageTokens } from 'umbot';
import { MyPlatformApi } from './MyPlatformApi';
async function myPlatformCardProcessing(cardInfo: ICardInfo, controller: BotController) {
const elements = [];
for (const image of cardInfo.images) {
// Если токена еще нет, загружаем его
if (!image.imageToken && image.imageDir) {
image.imageToken = await getImageToken(
image.imageDir,
'my_platform', // имя платформы
controller,
async (model: ImageTokens) => {
// 1. Загружаем файл в API платформы
const api = new MyPlatformApi(controller.appContext);
const uploadResult = await api.uploadImage(image.imageDir);
if (uploadResult?.id) {
// 2. Сохраняем токен в модель
model.imageToken = uploadResult.id;
// 3. Сохраняем модель в БД (чтобы в следующий раз не грузить заново)
if (await model.save(true)) {
return model.imageToken;
}
}
return null;
},
);
}
if (image.imageToken) {
elements.push({
type: 'image',
photo_id: image.imageToken,
title: image.title,
description: image.desc,
});
}
}
return elements;
}
Аналогично изображениям, используется утилита getSoundToken и модель SoundTokens.
import { getSoundToken, SoundTokens } from 'umbot';
// Внутри процессора звуков:
const audioToken = await getSoundToken(
path,
'my_platform',
controller,
async (model: SoundTokens) => {
const api = new MyPlatformApi(controller.appContext);
const res = await api.uploadAudio(path);
if (res?.id) {
model.soundToken = res.id;
if (await model.save(true)) return model.soundToken;
}
return null;
},
);
Некоторые платформы (Алиса, SmartApp) умеют хранить состояние диалога на своей стороне. Это позволяет не делать лишних запросов в БД.
Чтобы поддержать это, нужно реализовать 3 метода:
isLocalStorage(controller) — возвращает true, если платформа поддерживает локальное хранилище.getLocalStorage(controller) — возвращает данные, которые платформа прислала в запросе (обычно лежат в controller.state).setLocalStorage(data, controller) — вызывается фреймворком, если нужно сохранить данные на стороне платформы (если платформа не делает это автоматически через ответ).Нюанс: В setQueryData вы должны указать, в какое поле ответа класть стейт, заполнив controller.platformOptions.stateName (например, 'session_state' или 'user_state_update').
Задача: Собрать финальный ответ согласно контракту платформы.
Метод принимает controller (со всей бизнес-логикой, текстом, кнопками) и stateData (данные для локального хранилища).
Здесь есть две парадигмы ответов:
Парадигма А: Webhook-Response (Алиса, SmartApp) Платформа ждет JSON в теле HTTP-ответа.
async getContent(controller: BotController, stateData?: any): Promise<object> {
// 1. Собираем UI через наши процессоры
const buttons = controller.buttons.getButtons(myPlatformButtonProcessing);
const cards = await controller.card.getCards(myPlatformCardProcessing, controller);
// 2. Формируем ответ
const response = {
text: Text.resize(controller.text, 1024), // ОБЯЗАТЕЛЬНО режьте текст по лимитам!
tts: controller.tts,
buttons: buttons,
card: cards,
end_session: controller.isEnd
};
// 3. Добавляем состояние (если платформа его поддерживает)
if (controller.platformOptions.stateName && stateData) {
response[controller.platformOptions.stateName] = stateData;
}
return response;
}
Парадигма Б: API-Call (Telegram, VK, Max) Платформа ждет, что вы сами отправите ответ через её API, а вебхуку нужно просто вернуть 200 OK.
async getContent(controller: BotController): Promise<string> {
// 1. Если ответ еще не отправлен (флаг skipAutoReply)
if (!controller.skipAutoReply) {
const api = new MyPlatformApi(controller.appContext);
// Собираем все UI-компоненты
const keyboard = controller.buttons.getButtons(myPlatformButtonProcessing);
const attachments = await controller.card.getCards(myPlatformCardProcessing, controller);
const sounds = await controller.sound.getSounds(controller.tts, mySoundProcessing, controller);
// Передаем их в API платформы (формат зависит от самой платформы)
await api.sendMessage(controller.userId, Text.resize(controller.text, 4096), {
keyboard,
attachments, // Пример для Discord/VK
audio: sounds // Пример
});
}
// 3. Возвращаем заглушку для вебхука
return 'ok';
}
Возвращаемое значение из getContent пойдет в тело HTTP-ответа на вебхук. Если платформа требует специфичный JSON-ответ на сам факт получения вебхука (даже если вы уже отправили сообщение через API) — верните этот JSON. Если платформа принимает любой статус 200 OK — просто верните строку 'ok' или пустой объект.
Для локального тестирования через BotTest определите метод getQueryExample.
Этот метод эмулирует запрос от платформы, позволяя проверить работу приложения до деплоя.
Важно: Формат возвращаемого объекта должен точно соответствовать структуре запроса,
которую вы парсите в setQueryData.
// Для тестирования через BotTest
getQueryExample(
query: string,
userId: string,
count: number,
state: Record<string, unknown> | string,
): Record<string, unknown> {
// Возвращаем объект в формате ВАШЕЙ платформы
// Этот же формат будет парситься в setQueryData
return {
message: {
sender: { user_id: userId },
body: {
text: query,
seq: count,
},
},
state: state,
};
}
setQueryData:if (query.text === 'ping') {
// Фреймворк увидит sendInInit и сразу вернет этот ответ, пропустив логику бота
controller.platformOptions.sendInInit = { response: { text: 'pong' } };
return true;
}
Заполните controller.platformOptions.sendInInit объектом или строкой, которую платформа ожидает в качестве ответа на пинг
rateLimiter.export class MyPlatformAdapter extends BasePlatform {
limit = 30; // Сообщаем фреймворку о лимите
}