From 89fecf054b84c115a2b36598504abb22ae10ffb5 Mon Sep 17 00:00:00 2001 From: max36895 Date: Sat, 20 Jun 2026 12:41:36 +0300 Subject: [PATCH 1/2] doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены инструкции по работе с адаптерами --- src/docs/adapter/dbAdapter.md | 255 ++++++++++++++++++++++++ src/docs/adapter/platformAdapter.md | 296 ++++++++++++++++++++++++++++ src/docs/adapter/readme.md | 153 ++++++++++++++ 3 files changed, 704 insertions(+) create mode 100644 src/docs/adapter/dbAdapter.md create mode 100644 src/docs/adapter/platformAdapter.md create mode 100644 src/docs/adapter/readme.md diff --git a/src/docs/adapter/dbAdapter.md b/src/docs/adapter/dbAdapter.md new file mode 100644 index 0000000..8865a7c --- /dev/null +++ b/src/docs/adapter/dbAdapter.md @@ -0,0 +1,255 @@ +# Адаптеры баз данных (DB Adapters) + +Фреймворк umbot не знает, используете вы SQL, NoSQL или файловую систему. Он оперирует абстрактными объектами IQuery и IQueryData. Ваша задача как разработчика адаптера — написать "транслятор", который превращает эти абстракции в реальные запросы к вашей СУБД. + +## Архитектура: Template Method + +Базовый класс `BaseDbAdapter` (из `umbot/plugins`) берет на себя рутину: + +- Замер времени выполнения запросов (метрики EMetric.DB_SELECT, DB_INSERT и т.д.). +- Управление жизненным циклом (вызов connect при старте). +- Обертки над вашими методами (публичные `select`, `insert` вызывают ваши `_select`, `_insert`). + +### Почему мы переопределяем \_select, а не select? + +Публичные методы (`select`, `insert`, `update`, `remove`) в `BaseDbAdapter` уже написаны. Они оборачивают ваши внутренние методы (`_select`, `_insert`), чтобы замерять время выполнения и логировать метрики. Если вы переопределите select(), вы сломаете сбор метрик и логику повторных подключений. Вы всегда реализуете только методы с подчеркиванием. + +## Обязательный контракт (что нужно реализовать) + +Наследуемся от `BaseDbAdapter` и реализуем: + +1. connect(): Promise — Устанавливаете соединение с БД. +2. isConnected(): Promise — Проверяете, живо ли соединение (например, делаете ping БД). +3. \_select(selectData: IQuery, where: IQueryData | null, isOne: boolean): Promise — Поиск. +4. \_insert(insertData: IQuery): Promise — Добавление. +5. \_update(updateData: IQuery): Promise — Обновление. +6. \_remove(removeData: IQuery): Promise — Удаление. +7. destroy(): Promise — Закрываете пул соединений при остановке приложения. + +## Форматы данных (Шпаргалка): + +### Вход (IQuery): То, что фреймворк передает вам. + +Это объект, который фреймворк передает в ваши методы \_select, \_insert и т.д. + +```ts +{ + tableName: 'UsersData', // Имя таблицы/коллекции + primaryKeyName: 'userId', // Первичный ключ + query: { userId: '123' }, // Условия WHERE (может быть null) + data: { name: 'John' }, // Данные для SET (может быть null) + rules: [{ name: ['name'], type: 'string', max: 50 }] // Правила валидации +} +``` + +### Условия и данные (IQueryData) + +Формат query и data внутри IQuery. +Важно: Значения могут быть не только примитивами, но и объектами с операторами. Фреймворк не навязывает конкретный диалект (например, $gt для Mongo или > для SQL). Адаптер сам решает, как интерпретировать эти операторы. + +```ts +// Простое условие (равенство) +{ userId: '123', platform: 'alisa' } + +// Условие с оператором (адаптер должен сам распарсить это в SQL `age > 18` или Mongo `$gt`) +{ age: { $gt: 18 }, status: 'active' } +``` + +### Выходные данные (IModelRes) + +То, что вы обязаны вернуть из метода `_select`. + +```ts +// Успех (даже если ничего не найдено, status должен быть true, а data - пустым массивом или null) +{ status: true, data: { userId: '123', name: 'John' } } +{ status: true, data: [] } + +// Ошибка (сбой подключения, синтаксическая ошибка и т.д.) +{ status: false, error: 'Connection timeout' } +``` + +Для методов `_insert`, `_update`, `_remove` вы возвращаете просто boolean (true при успехе, false при ошибке). + +## Критические нюансы (Скрытые контракты) + +### 1. Валидация данных + +В базовом классе `BaseDbAdapter` нет встроенного метода `validate()`. +Однако в `MongoAdapter` он реализован для валидации данных по правилам модели (`IModelRules`). + +Если вы хотите, чтобы ваш адаптер также валидировал данные (обрезал строки по `max`, +приводил типы), реализуйте метод `validate()` в своём классе: + +```ts +public validate(query: IQuery, element: IQueryData | null): IQueryData { + if (!element) return {}; + + const rules = query.rules; + if (rules) { + rules.forEach((rule) => { + rule.name.forEach((fieldName) => { + if (rule.type === 'string' || rule.type === 'text') { + if (rule.max !== undefined) { + element[fieldName] = Text.resize(element[fieldName] as string, rule.max); + } + element[fieldName] = this.escapeString(element[fieldName] as string); + } else if (rule.type === 'integer' || rule.type === 'int') { + element[fieldName] = +(element[fieldName] as number); + } + }); + }); + } + return element; +} +``` + +Затем вызывайте его в `_insert()` и `_update()`: + +```ts +public async _insert(insertData: IQuery): Promise { + const validData = this.validate(insertData, insertData.data); + // ... выполнение запроса с validData +} +``` + +**Примечание**: Валидация в модели (`Model.validate()`) и в адаптере (`validate()`) — это разные вещи. +Модель валидирует свои данные перед сохранением, а адаптер валидирует данные по правилам `IModelRules` +перед выполнением запроса к БД. + +_Зачем тогда в `IQuery` передаются `rules`?_ +Они нужны вам для **маппинга типов** специфичных для вашей СУБД. Например, если вы пишете SQL-адаптер, вы можете использовать `rules`, чтобы понять, что поле с `type: 'object'` нужно сериализовать в JSON-строку перед вставкой, а `max: 150` использовать для динамического создания `VARCHAR(150)`. + +### 2. Хранение подключения (Connection Pool) + +Чтобы не создавать новое подключение к БД на каждый запрос, фреймворк предоставляет синглтон-хранилище. +При успешном `connect()` вы должны сохранить пул соединений в `this._appContext.database.databaseInfo`. + +```ts +async connect(): Promise { + const pool = await createMyDbPool(this._dbOptions); + // Сохраняем пул, чтобы использовать его в _select/_insert + this._appContext.database.databaseInfo = { myDbPool: pool }; + return true; +} +``` + +### 3. Произвольные запросы (\_query) + +Если разработчику приложения нужно выполнить "сырой" SQL-запрос или агрегацию, он использует метод `model.query(callback)`. +В `BaseDbAdapter` публичный `query` просто вызывает `_query`. По умолчанию `_query` возвращает `null`. Если вы хотите поддержать кастомные запросы, переопределите `_query`, передав в callback ваше подключение. + +### 4. Обработка ошибок + +Не бросайте исключения (`throw new Error`) из методов `_select`, `_insert`, `_update`, `_remove`. +Фреймворк ожидает, что вы сами обработаете ошибки внутри метода и вернете `false` или `{ status: false, error: ... }`. + +## Универсальный пример реализации (Псевдокод) + +```ts +import { BaseDbAdapter } from 'umbot/plugins'; +import { IQuery, IQueryData, IModelRes, TQueryCb } from 'umbot'; + +export class MyCustomDbAdapter extends BaseDbAdapter { + async connect(): Promise { + try { + // 1. Создаем пул соединений + const pool = await myDbDriver.connect(this._dbOptions); + // 2. Сохраняем его в контекст + this._appContext.database.databaseInfo = { pool }; + return true; + } catch (err) { + return false; + } + } + + async isConnected(): Promise { + const pool = this._appContext.database.databaseInfo?.pool; + return pool ? await pool.ping() : false; + } + + async _select( + selectData: IQuery, + where: IQueryData | null, + isOne: boolean, + ): Promise { + const pool = this._appContext.database.databaseInfo?.pool; + if (!pool) return { status: false, error: 'No DB connection' }; + + try { + // 1. Парсим абстрактные условия where в SQL/NoSQL запрос + const sqlQuery = this.buildSelectQuery(selectData.tableName, where, isOne); + + // 2. Выполняем запрос + const result = await pool.execute(sqlQuery); + + // 3. Возвращаем в формате IModelRes + return { status: true, data: result }; + } catch (err) { + // Не бросаем исключение, а возвращаем статус false + return { status: false, error: (err as Error).message }; + } + } + + async _insert(insertData: IQuery): Promise { + const pool = this._appContext.database.databaseInfo?.pool; + if (!pool) return false; + + try { + // Валидация (так как в BaseDbAdapter её нет, используем свою) + const validData = this.validate(insertData, insertData.data); + const sqlQuery = this.buildInsertQuery(insertData.tableName, validData); + await pool.execute(sqlQuery); + return true; + } catch (err) { + return false; + } + } + + // Переопределяем _query, чтобы поддержать сырые запросы от разработчика + public async _query(callback: TQueryCb): Promise { + const pool = this._appContext.database.databaseInfo?.pool; + if (pool) { + // Передаем пул в callback разработчика + return await callback(pool, pool); + } + return null; + } + + async destroy(): Promise { + const pool = this._appContext.database.databaseInfo?.pool; + if (pool) await pool.close(); + } + + // --- Вспомогательные методы --- + + // Своя валидация + private validate(query: IQuery, data: IQueryData | null): IQueryData { + if (!data) return {}; + // Здесь можно пройтись по query.rules и обрезать строки по max + return data; + } + + // Транслятор IQueryData в SQL (упрощенно) + private buildSelectQuery(table: string, where: IQueryData | null, isOne: boolean): string { + let sql = `SELECT * FROM ${table}`; + if (where) { + const conditions = Object.keys(where).map((key) => { + const val = where[key]; + // Поддержка операторов + if (typeof val === 'object' && val !== null && val.$gt !== undefined) { + return `${key} > ${val.$gt}`; + } + return `${key} = '${val}'`; + }); + sql += ` WHERE ${conditions.join(' AND ')}`; + } + if (isOne) sql += ' LIMIT 1'; + return sql; + } + + private buildInsertQuery(table: string, data: IQueryData): string { + // ... логика формирования INSERT + return ''; + } +} +``` diff --git a/src/docs/adapter/platformAdapter.md b/src/docs/adapter/platformAdapter.md new file mode 100644 index 0000000..b0adb79 --- /dev/null +++ b/src/docs/adapter/platformAdapter.md @@ -0,0 +1,296 @@ +# Создание адаптера платформы (Platform Adapter) + +Адаптер платформы — это мост между сырым JSON/XML запросом от внешней платформы и унифицированным контроллером `BotController`. Ваша задача: распарсить входящие данные, наполнить контроллер, обработать UI-компоненты (кнопки, картинки, звуки) и сформировать ответ строго по контракту конкретной платформы. + +Адаптер наследуется от базового класса BasePlatform (из `umbot/plugins`). + +## Инициализация и идентификация платформы + +Когда на сервер приходит запрос, фреймворк перебирает все подключенные адаптеры и спрашивает: «Это твой запрос?». + +### Определение платформы (isPlatformOnQuery) + +Вы должны реализовать метод, который по заголовкам или телу запроса понимает, относится ли он к вашей платформе. + +**Пример**: Платформа WeChat отправляет специфичный заголовок x-wechat-signature и XML в теле. Telegram отправляет заголовок x-telegram-bot-api-secret-token. + +```ts +isPlatformOnQuery(query: any, headers?: Record): 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` и реализуйте свою логику валидации + +## Парсинг запроса (setQueryData) + +Задача: Взять сырой `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` — дополнительные данные (например, нажатая кнопка). + +```ts +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; +} +``` + +## Работа с UI-компонентами (Кнопки, Карточки, Звуки) + +Фреймворк оперирует абстракциями (`IButtonType`, `ICardInfo`). Платформы требуют специфичные форматы. Чтобы превратить абстракцию в формат платформы, используются функции-процессоры. + +### Кнопки + +Вам нужно написать функцию, которая принимает массив абстрактных кнопок и возвращает объект, понятный платформе. +Метод `controller.buttons.getButtons(ваш_процессор)` сам вызовет вашу функцию и отдаст результат. + +```ts +// 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 платформы и сохраняете токен в БД. + +```ts +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`. + +```ts +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; + }, +); +``` + +## Управление состояниями (State / Local Storage) + +Некоторые платформы (Алиса, SmartApp) умеют хранить состояние диалога на своей стороне. Это позволяет не делать лишних запросов в БД. + +Чтобы поддержать это, нужно реализовать 3 метода: + +1. `isLocalStorage(controller)` — возвращает true, если платформа поддерживает локальное хранилище. +2. `getLocalStorage(controller)` — возвращает данные, которые платформа прислала в запросе (обычно лежат в controller.state). +3. `setLocalStorage(data, controller)` — вызывается фреймворком, если нужно сохранить данные на стороне платформы (если платформа не делает это автоматически через ответ). + +Нюанс: В `setQueryData` вы должны указать, в какое поле ответа класть стейт, заполнив `controller.platformOptions.stateName` (например, 'session_state' или 'user_state_update'). + +## Формирование ответа (getContent) + +**Задача:** Собрать финальный ответ согласно контракту платформы. +Метод принимает `controller` (со всей бизнес-логикой, текстом, кнопками) и `stateData` (данные для локального хранилища). +Здесь есть две парадигмы ответов: + +**Парадигма А:** Webhook-Response (Алиса, SmartApp) +Платформа ждет JSON в теле HTTP-ответа. + +```ts +async getContent(controller: BotController, stateData?: any): Promise { + // 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. + +```ts +async getContent(controller: BotController): Promise { + // 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' или пустой объект. + +### Тестовый набор данных(getQueryExample) + +Для локального тестирования через `BotTest` определите метод `getQueryExample`. +Этот метод эмулирует запрос от платформы, позволяя проверить работу приложения до деплоя. + +**Важно:** Формат возвращаемого объекта должен точно соответствовать структуре запроса, +которую вы парсите в `setQueryData`. + +```ts +// Для тестирования через BotTest +getQueryExample( + query: string, + userId: string, + count: number, + state: Record | string, +): Record { + // Возвращаем объект в формате ВАШЕЙ платформы + // Этот же формат будет парситься в setQueryData + return { + message: { + sender: { user_id: userId }, + body: { + text: query, + seq: count, + }, + }, + state: state, + }; +} +``` + +## Рекомендации и оптимизации (Не обязательно, но желательно) + +1. **Healthcheck (Ping/Pong):** Платформы периодически шлют пустые запросы или слово ping, чтобы проверить, что сервер жив. Чтобы не грузить БД и логику бота, перехватывайте это в `setQueryData`: + +```ts +if (query.text === 'ping') { + // Фреймворк увидит sendInInit и сразу вернет этот ответ, пропустив логику бота + controller.platformOptions.sendInInit = { response: { text: 'pong' } }; + return true; +} +``` + +Заполните `controller.platformOptions.sendInInit` объектом или строкой, которую платформа ожидает в качестве ответа на пинг + +2. **Лимиты платформы (Rate Limit):** Если у платформы есть жесткий лимит запросов в секунду (например, 30 req/sec у Telegram/Max), укажите это в классе адаптера. Фреймворк автоматически подключит встроенный `rateLimiter`. + +```ts +export class MyPlatformAdapter extends BasePlatform { + limit = 30; // Сообщаем фреймворку о лимите +} +``` + +3. **Соблюдение таймаутов:** Платформы (Алиса, Сбер) дают максимум 3 секунды на ответ. В BasePlatform уже вшита проверка времени: если ваш getContent выполняется слишком долго, фреймворк сам запишет ошибку/предупреждение в логи. Просто не делайте тяжелых синхронных операций внутри getContent. diff --git a/src/docs/adapter/readme.md b/src/docs/adapter/readme.md new file mode 100644 index 0000000..669e051 --- /dev/null +++ b/src/docs/adapter/readme.md @@ -0,0 +1,153 @@ +# Архитектура расширений: Плагины и Адаптеры + +В `umbot` нет жесткого разделения на "адаптеры" и "плагины" на уровне ядра. Всё, что расширяет функционал, реализует интерфейс `IPlugin` (или `IPluginFn`) и подключается через `bot.use()`. Адаптеры для БД и Платформ — это просто специализированные плагины, наследующие базовые классы. + +## Как это работает: + +При вызове `bot.use(entity)` фреймворк проверяет тип сущности. Если это класс, он вызывает `entity.init(appContext, bot)`. Контекст (`AppContext`) — это синглтон-хаб, где хранятся состояния, токены, реестры команд и подключенные модули. + +## Типы расширений: + +1. Функциональные плагины (i18n, NLU, кастомные RegExp). Регистрируются в `appContext.plugins`. +2. Кастомные плагины. Регистрируются в `appContext.plugins` под любым вашим ключом. Вы вызываете их вручную из своего кода. +3. Адаптеры БД (Mongo, File). Регистрируются в `appContext.database.adapter`. +4. Платформенные адаптеры (Telegram, Alisa, VK). Регистрируются в `appContext.platforms`. + +## Как написать свой функциональный плагин + +Вам не нужно наследоваться от базовых классов. Достаточно реализовать функцию или объект и поместить его в нужный слот `appContext.plugins`. + +### Вариант 1: Функция-плагин (рекомендуется) + +```ts +import { Bot, AppContext, IPluginFn } from 'umbot'; + +const myI18nPlugin: IPluginFn = (appContext: AppContext, bot: Bot) => { + // Регистрируем плагин в слоте 'i18n' + appContext.plugins['i18n'] = (key: string, ...params: unknown[]) => { + return `Перевод для: ${key}`; + }; + + // Возвращаем функцию очистки ресурсов (опционально) + return () => { + // Освобождение ресурсов при уничтожении плагина + console.log('i18n plugin destroyed'); + }; +}; + +const bot = new Bot(); +bot.use(myI18nPlugin); +``` + +### Вариант 2: Класс-плагин + +```ts +import { Bot, AppContext, IPlugin } from 'umbot'; + +class MyI18nPlugin implements IPlugin { + init(appContext: AppContext, bot: Bot): void { + appContext.plugins['i18n'] = (key: string, ...params: unknown[]) => { + return `Перевод для: ${key}`; + }; + } + + destroy(bot: Bot): void { + // Освобождение ресурсов + console.log('i18n plugin destroyed'); + } +} + +const bot = new Bot(); +bot.use(new MyI18nPlugin()); +``` + +## Сигнатуры системных плагинов + +Для корректной работы системных плагинов соблюдайте их сигнатуры: + +| Плагин | Сигнатура | Описание | +| ------ | ----------------------------------------------------------------------------- | ------------------------------------------- | +| i18n | (key: string, ...params: unknown[]) => string | Локализация текста | +| nlu | (text: string, platformNlu: INlu, platform: string, request: unknown) => INlu | Обработка естественного языка | +| regExp | () => RegExpConstructor | Кастомная реализация RegExp (например, re2) | + +### Пример i18n плагина + +```ts +const i18nPlugin: IPluginFn = (appContext) => { + const translations = { + hello: 'Привет', + bye: 'Пока', + }; + + appContext.plugins['i18n'] = (key: string) => { + return translations[key] || key; + }; +}; +i18nPlugin.isPlugin = true; + +bot.use(i18nPlugin); +``` + +### Пример NLU плагина + +```ts +const nluPlugin: IPluginFn = (appContext) => { + appContext.plugins['nlu'] = ( + text: string, + platformNlu: INlu, + platform: string, + request: unknown, + ) => { + // Обогащаем NLU данными из внешнего сервиса + return { + ...platformNlu, + intents: { + ...platformNlu.intents, + custom_intent: { slots: [] }, + }, + }; + }; +}; +nluPlugin.isPlugin = true; + +bot.use(nluPlugin); +``` + +## Как написать свой уникальный плагин (Кастомный) + +Вы можете создать плагин для любых задач (кэширование, работа с внешним API, логика бизнес-правил) и зарегистрировать его под своим именем. + +```ts +// 1. Создаем и регистрируем плагин +const myCustomCachePlugin: IPluginFn = (appContext: AppContext) => { + const cache = new Map(); + + // Регистрируем под своим уникальным ключом + appContext.plugins['myCustomCache'] = { + set: (key: string, value: any) => cache.set(key, value), + get: (key: string) => cache.get(key), + }; +}; +myCustomCachePlugin.isPlugin = true; // Обязательный маркер + +bot.use(myCustomCachePlugin); + +// 2. Обращаемся к нему из своего кода (например, в команде или контроллере) +bot.addCommand('save_data', ['сохрани'], (text, controller) => { + // Получаем доступ к нашему плагину через appContext + const cache = controller.appContext.plugins['myCustomCache']; + if (cache) { + cache.set('last_command', text); + controller.text = 'Данные сохранены в кастомный кэш!'; + } +}); +``` + +## Жизненный цикл плагина + +1. **Инициализация:** При вызове `bot.use(plugin)` вызывается `plugin.init(appContext, bot)`. +2. **Работа:** Плагин регистрирует свои обработчики в `appContext.plugins`. +3. **Уничтожение:** При вызове `bot.clearUse()` или завершении приложения вызывается `plugin.destroy(bot)`. + +Важно: Метод destroy используется для освобождения ресурсов (закрытие соединений, отписка от событий, очистка таймеров). Если ваш плагин не создает ресурсов, метод можно не реализовывать. From 83d4f926df68154a1685a5bc726b1cde17ae356d Mon Sep 17 00:00:00 2001 From: "ma.mochalov" Date: Sat, 20 Jun 2026 15:23:35 +0300 Subject: [PATCH 2/2] =?UTF-8?q?v-3.0.13=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B5=D0=B4=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=8E=D1=89=D0=B8=D0=B5=20=D1=8D=D0=BA=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package.json | 2 +- src/plugins/index.ts | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4ad861d..249dff5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ /dist/* /docs/* /node_modules/* +/reps/* /coverage/* /package-lock.json diff --git a/package.json b/package.json index 6cb1471..1ea04f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umbot", - "version": "3.0.12", + "version": "3.0.13", "description": "Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платформы без изменения основного кода.", "keywords": [ "vk", diff --git a/src/plugins/index.ts b/src/plugins/index.ts index c4492be..a1ea604 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -2,6 +2,8 @@ export { BasePlatform as BasePlatformAdapter, type TContent, type IOptions as IAdapterOptions, + EMPTY_CONTEXT_ERROR, + EMPTY_QUERY_ERROR, } from './platforms/Base/Base'; export * as pUtils from './platforms/Base/utils';