# 09. Плагины

# Плагины

### Инструкция пользователя

**Плагины Metabot** — это JavaScript библиотеки, скрипты которых доступны для повторного использования в различных скриптах ботов.

Существует два вида плагинов:

- **Плагины бизнеса** — это собственные JavaScript библиотеки, созданные вами и доступные в любом боте вашего бизнеса. Другим бизнесам эти библиотеки не доступны;
- **Общие (предопределенные) плагины** — это JavaScript библиотеки доступные в любом бизнесе, любого бота. Разработкой таких библиотек занимается команда Metabot.

Другими словами, вы можете создать свои собственные JavaScript библиотеки или использовать предопределенные, готовые библиотеки.

### Список общих плагинов

<table border="1" id="bkmrk-%D0%9D%D0%B0%D0%B8%D0%BC%D0%B5%D0%BD%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%9F%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87" style="border-collapse: collapse; width: 100.001%; height: 191.296px;"><tbody><tr style="background-color: #c2e0f4; height: 29.537px;"><td style="width: 24.9607%; height: 29.537px;">Наименование</td><td style="width: 19.0348%; height: 29.537px;">Подключение</td><td style="width: 30.8867%; height: 29.537px;">Пример</td><td style="width: 24.9607%; height: 29.537px;">Дополнительно</td></tr><tr style="height: 46.3426px;"><td style="width: 24.9607%; height: 46.3426px;">moment </td><td style="width: 19.0348%; height: 46.3426px;">require()</td><td style="width: 30.8867%; height: 46.3426px;">let moment = require('moment')</td><td style="width: 24.9607%; height: 46.3426px;">  
</td></tr><tr style="height: 66.1111px;"><td style="width: 24.9607%; height: 66.1111px;">moment-with-locales

</td><td style="width: 19.0348%; height: 66.1111px;">require()</td><td style="width: 30.8867%; height: 66.1111px;">let moment = require('moment-with-locales')</td><td style="width: 24.9607%; height: 66.1111px;">Это OpenSource библиотека для работы с датами, [https://momentjs.com/](https://momentjs.com/)

Версия библиотеки moment.js: 2.29.3

</td></tr><tr style="height: 49.3056px;"><td style="width: 24.9607%; height: 49.3056px;">Common.Bot.Commands

</td><td style="width: 19.0348%; height: 49.3056px;">require() | snippet()</td><td style="width: 30.8867%; height: 49.3056px;">require('Common.Bot.Commands') | snippet('Common.Bot.Commands')</td><td style="width: 24.9607%; height: 49.3056px;">  
</td></tr></tbody></table>

<p class="callout warning">Если для вашей ситуации доступно подключение через **require** – то это **более предпочтительный** с точки зрения производительности вариант, используйте именно его. Если подключение с помощью require() недоступно или не приемлемо, то используйте подключение через snippet().</p>

<p class="callout warning">На данный момент подключение **собственных библиотек** бизнеса доступно **только через snippet()**.</p>

### Интерфейс и логика настройки плагинов

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/itLCfn7RtDCPMh6s-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/itLCfn7RtDCPMh6s-image.png)

<div id="bkmrk-%D0%98%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81-%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%BE%D0%B2-%D0%BD">Интерфейс плагинов на платформе Metabot24 состоит из двух уровней:</div>- **Плагины** — в данном разделе настраивается общее описание библиотеки и пространство имен для скриптов вложенных в плагин. Название плагина похоже на название директории в которой хранится ваша библиотека;

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/8QLUXzXmlk4jVttq-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/8QLUXzXmlk4jVttq-image.png)

- **Скрипты плагинов** — это сами скрипты которые вы можете использовать в своих ботах. Название соответствует названию файла к которому вы обращаетесь.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/LoG1Po3i5qJw0Z3g-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/LoG1Po3i5qJw0Z3g-image.png)

<div id="bkmrk-%D0%9F%D0%BE%D0%BB%D0%BD%D0%BE%D0%B5-%D0%B8%D0%BC%D1%8F-%D0%B1%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA">Полное имя библиотеки «клеится» из трех составляющих:</div><div id="bkmrk--24"></div>- Уровень доступа (Common или Business);
- Название плагина;
- Название скрипта плагина.

При подключении библиотеки «составляющие» разделяются точкой, а при обращении к методам библиотеки из JS «составляющие клеятся» вместе, т.е. пишутся слитно, без точки.

### Сниппеты

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

Доступны два варианта подключения сниппета:

```JavaScript
snippet("your_snippet_name")
или
[[:your_snippet_name:]]
```

<div id="bkmrk-%D0%9E%D0%B1%D0%B0-%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%D0%B0-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%8E"><div><div>Оба варианта работают как макрос, т.е.указанный вариант обьявления сниппета в JavaScript коде будет заменен на код, который указан в скрипте плагина, к которому мы обращаемся.</div><div style="padding-left: 40px;">  
</div><div>В содержимом скрипта плагина можно указывать любой JavaScript код, главное чтобы ваш код, в котором выполняется макроподстановка сниппета был валиден после замены</div><div>  
</div><div>Рассмотрим сначала простой пример указания значения переменной, вместо JavaScript. Вместо JavaScript кода сниппета можно указать любую комбинацию, например:</div><div style="padding-left: 80px;">  
</div></div></div>```JavaScript
let text = snippet("mysnippet");
```

<div id="bkmrk-%D0%90-%D0%B2-%D1%81%D0%B0%D0%BC%D0%BE%D0%BC-%D1%81%D0%BD%D0%B8%D0%BF%D0%BF%D0%B5%D1%82%D0%B5-%D1%83"><div><div>А в самом сниппете указать любое значение (число, текст, булево и т.п.), например, если в сниппете мы указали код:</div><div style="padding-left: 80px;">  
</div></div></div>```JavaScript
"Hello World!"
```

<div id="bkmrk-%D1%82%D0%BE%2C-%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA%D0%B0%D0%B5%D0%BC%D1%8B%D0%B9-js-%D0%B8"><div><div>То, запускаемый JS интерпретатором код примет следующий вид:</div><div style="padding-left: 80px;">  
</div></div></div>```JavaScript
let text = "Hello World!"
```

<div id="bkmrk-%D0%9E%D0%B1%D1%80%D0%B0%D1%82%D0%B8%D1%82%D0%B5-%D0%B2%D0%BD%D0%B8%D0%BC%D0%B0%D0%BD%D0%B8%D0%B5%2C-%D1%87"><div><div>Обратите внимание, что символ точка с запятой является частью определения макроса, и может указываться после snippet, а может не указываться. Если вам необходимо чтобы «конечный исходный код», после замены содержал точку с запятой, то укажите ее в самом JavaScript коде сниппета (скрипта плагина), т.е.:</div><div style="padding-left: 80px;">  
</div></div></div>```JavaScript
"Hello World!";
```

<div id="bkmrk-%D1%82%D0%BE%D0%B3%D0%B4%D0%B0%2C-%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA%D0%B0%D0%B5%D0%BC%D1%8B%D0%B9-j"><div><div>Тогда, запускаемый JS интерпритатором код примет следующий вид:</div><div style="padding-left: 80px;">  
</div></div></div>```JavaScript
let text = "Hello World!";
```

<p class="callout warning">Сниппет идентичен макроподстановке. Макроподстановка выполняется до запуска скрипта и не является командой интерпретатора JavaScript, поэтому если вы закомментируете объявление сниппета, то он все равно будет подставлен в исходный код ! Чтобы закомментировать сниппет нужно закомментировать его и нарушить синтаксис его объявления, чтобы система не нашла макрос со сниппетом и не выполнила макроподстановку, например написать: s!nippet("your\_snippet\_name").</p>

### Пример №1: Общий плагин (скрипт общего плагина)

Полное имя библиотеки «клеится» из трех составляющих:

- Общий плагин (Common);
- Название плагина «Bot»;
- Скрипт «Commands».

Подключается такая библиотеки с помощью кода:

```JavaScript
require('Common.Bot.Commands');
```

После подключения, в JavaScript автоматически будет доступна переменная **CommonBotCommands**.

К методам CommonBotCommands можно обращаться, например для отправки сообщения в мессенджер из JavaScript:

```JavaScript
CommonBotCommands.SendText('Текст отправленный из V8');
```

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

В данном примере речь идет именно об общей библиотеке (Common), внутри которой уже описан специальный объект, который можно использовать с помощью методов, чтобы добиться такого же варианта использования вы должны самостоятельно описать свой объект на JavaScript. Оформление в виде JavaScript обьекта при создании вашего собственного плагана бизнеса не является обязательным, за счет того что ваши скрипты будут подключаться в виде сниппетов. Вы можете просто описать любые переменные и функции в вашей библиотеке и обращаться к ним по имени.

<p class="callout warning">Для библиотек уровня бизнеса автоматически создаваемые переменные-объекты не доступны (или доступны только в тех библиотеках которые для вас создаст команда Metabot).</p>

Отметим, что существуют другие библиотеки, например, такие как moment.js. Подключение и использование таких библиотек отличается от использования библиотек созданных на платформе. Например, для moment.js, не нужно указывать три «состявляющие» названия скрипта (уровень доступа, плагин и скрипт). Для moment.js мы явно объявляем переменную и «экспортируем в нее библиотеку»:

```JavaScript
let moment = require('moment');
```

Подключение различных библиотек может отличаться и зависит от реализации самой библиотеки. Но, в основном как подключать и будет ли доступна автоматически переменная-объект понятно из названия библиотеки (если при подключении нет указания уровня доступа, то скорее всего это OpenSource библиотека).

<p class="callout warning">Если вы подключаете к одному скрипту несколько сниппетов, то переменные обьявленые внутри каждого из сниппетов не должны пересекаться между собой по имени, а также переменные объявленные в сниппете и вашем скрипте, к которому вы подключаете сниппет не должны пересекаться.</p>

### Пример №2: Плагин бизнеса (скрипт вашего плагина)

Создадим новый плагин для формирования текста, который мы будет отправлять пользователю с помощью атрибутов бота.

<div id="bkmrk-%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D1%80-2-%D0%9F%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD-%D0%B1%D0%B8%D0%B7%D0%BD"><div><div id="bkmrk--34"></div></div></div>[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/LsooGEkAauywlOcR-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/LsooGEkAauywlOcR-image.png)

Заполним все необходимые поля.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/DVvu7qdhu0upjbVT-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/DVvu7qdhu0upjbVT-image.png)

Далее, перейдем в скрипты плагина.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/l4xCoFcdlDqyoA00-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/l4xCoFcdlDqyoA00-image.png)

И создадим новый скрипт.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/unRG1SiS9NtnE6J9-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/unRG1SiS9NtnE6J9-image.png)

Заполним в открывшемся окне необходимые поля.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/FjdAhSyfli17KuUt-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/FjdAhSyfli17KuUt-image.png)

<div id="bkmrk-%D0%A3%D0%BA%D0%B0%D0%B6%D0%B5%D0%BC-%D0%B2-%D0%B8%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE"><div><div id="bkmrk-%D0%A3%D0%BA%D0%B0%D0%B6%D0%B5%D0%BC-%D0%B2-%D0%B8%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE-0">Укажем в исходном коде скрипта:</div><div style="padding-left: 40px;">  
</div></div></div>```JavaScript
const greetMsg = 'Привет, ' + lead.getData('name') + '!';
memory.setAttr("greet", greetMsg);
```

Здесь мы сохраняем текст приветствия в memory (временном атрибуте бота).

Полное имя библиотеки «клеится» из трех составляющих:

- Плагин бизнеса (Business);
- Название плагина «Notifications»;
- Скрипт «HelloLead».

Подключается с помощью кода:

```JavaScript
snippet('Business.Notifications.HelloLead');
```

<div id="bkmrk--14"><div id="bkmrk-"><div id="bkmrk--27" style="padding-left: 40px;"></div><div>[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/DwMqRn88aSIVB74K-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/DwMqRn88aSIVB74K-image.png)  
</div></div></div><div id="bkmrk-%D0%94%D0%BB%D1%8F-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B8-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%8B-"><div class="align-left" id="bkmrk-%D0%94%D0%BB%D1%8F-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B8-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%8B--1">Для проверки работы плагина, в скрипт бота необходимо добавить две команды:</div>- **Выполнить JavaScript** — здесь мы подключаем сниппет, для заполнения атрибута(макропеременной) во временной памяти бота:

<div id="bkmrk--35"></div></div>```JavaScript
snippet('Business.Notifications.HelloLead');
```

<div id="bkmrk-%D0%97%D0%B4%D0%B5%D1%81%D1%8C-%D0%BC%D1%8B-%D0%BF%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87%D0%B0%D0%B5%D0%BC-"><div id="bkmrk--28" style="padding-left: 80px;"></div><div id="bkmrk--36" style="padding-left: 40px;"></div>- **Отправить текст** — в содержимом использована макропеременная, текст который будет отправлен в <div id="bkmrk-%D0%BC%D0%B5%D1%81%D1%81%D0%B5%D0%BD%D0%B4%D0%B6%D0%B5%D1%80.">мессенджер:</div>

<div id="bkmrk--37" style="padding-left: 40px;"></div></div>```JavaScript
{{ &$greet }}
```

<div id="bkmrk-%D0%92-%D1%81%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%BC%D0%BE%D0%BC-%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7"><div id="bkmrk-%D0%94%D0%BB%D1%8F-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B8-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%8B--0"><div id="bkmrk--29" style="padding-left: 80px;"></div><div id="bkmrk--40" style="padding-left: 40px;"></div><div id="bkmrk--0">[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/6nuflCzrzxF1unN6-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/6nuflCzrzxF1unN6-image.png)  
</div></div></div>Результат работы скрипта в телеграм:

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/VVQKGqswPxncg8Im-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/VVQKGqswPxncg8Im-image.png)

### Пример №3: Использование общего плагина в плагине бизнеса

Модифицируем пример 2 так, чтобы текст отправлялся не с помощью команды **Отправить текст**, а прям из JS кода плагина.

<div id="bkmrk-%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D1%80-3-%D0%98%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD"><div><div id="bkmrk-%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D1%80-3-%D0%98%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD-0"><div><div id="bkmrk--47"></div></div></div></div></div>[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/rg5cHiyVPXXxD0ba-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/rg5cHiyVPXXxD0ba-image.png)

<div id="bkmrk-%D0%92-%D0%B8%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE%D0%B4%D0%B5-%D1%81%D0%BA%D1%80%D0%B8"><div><div><div><div><div class="align-left" id="bkmrk-%D0%92-%D0%B8%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE%D0%B4%D0%B5-%D1%81%D0%BA%D1%80%D0%B8-1">В исходном коде скрипта укажем:</div><div class="align-left" style="padding-left: 80px;">  
</div></div></div></div></div></div>```JavaScript
require('Common.Bot.Commands');
const greetMsg = 'Привет, ' + lead.getData('name') + '!';
CommonBotCommands.sendText(greetMsg);
```

<div id="bkmrk--19"><div><div id="bkmrk-%D0%92-%D0%B8%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE%D0%B4%D0%B5-%D1%81%D0%BA%D1%80%D0%B8-0"><div><div class="align-left" id="bkmrk--31" style="padding-left: 80px;"></div></div></div></div></div>[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/eAhIyFwupdI0L0Sb-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/eAhIyFwupdI0L0Sb-image.png)

Удалим команду **Отправить текст**, команду **Выполнить JavaScript** оставляем без изменений.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/GPu6wvfCRJo3IMjt-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/GPu6wvfCRJo3IMjt-image.png)

Результат работы данного скрипта будет идентичным примеру 2:

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/z7Totuc3E5nlFShT-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/z7Totuc3E5nlFShT-image.png)

<div id="bkmrk--25"><div id="bkmrk--32"><div id="bkmrk--48"><div>  
</div></div></div></div>

# Библиотека плагинов



# Плагин для Mindbox

<table border="1" id="bkmrk-%D0%90%D0%B2%D1%82%D0%BE%D1%80-%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%B0-%D0%9F%D0%B5%D1%82%D1%80%D0%BE%D0%B2" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; width: 102.469%; height: 176.969px;"><tbody><tr style="height: 29.7969px;"><td style="width: 38.2087%; height: 29.7969px;">**Название плагина**</td><td style="width: 61.7894%; height: 29.7969px;">Mindbox</td></tr><tr style="height: 29.7969px;"><td style="width: 38.2087%; height: 29.7969px;">**Разработчик**</td><td style="width: 61.7894%; height: 29.7969px;">Официальные плагины от Metabot</td></tr><tr style="height: 57.7812px;"><td class="align-left" style="width: 38.2087%; height: 57.7812px;">**Авторы**</td><td style="width: 61.7894%; height: 57.7812px;">Петрова Ирина Дмитриевна (<ira.petrova@metabot.org>)

Гарашко Артем Юрьевич ([artem@metabot.org](mailto:artem@metabot.rg))

</td></tr><tr style="height: 29.7969px;"><td class="align-left" style="width: 38.2087%; height: 29.7969px;">**Дата создания**</td><td style="width: 61.7894%; height: 29.7969px;">22 Ноября 2022</td></tr><tr style="height: 29.7969px;"><td class="align-left" style="width: 38.2087%; height: 29.7969px;">**Последняя дата обновления**</td><td style="width: 61.7894%; height: 29.7969px;">26 Ноября 2022</td></tr></tbody></table>

### Описание

Mindbox — это платформа автоматизации маркетинга и клиентских данных. Этот плагин к платформе Metabot позволяет интегрировать ваш чат-бот, разрабатываемый на Metabot, с платформой Mindbox. Подробнее с Mindbox можно ознакомиться на их официальном сайте: [https://mindbox.ru](https://mindbox.ru/).

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

### Подключение

Для интеграции Mindbox с вашим чат-ботом вам необходимо сделать несколько вещей:

1. На стороне Mindbox настройте все нужные точки интеграции и операции. Подробнее смотрите в [документации Mindbox](https://help.mindbox.ru/docs/%D0%BA%D0%B0%D0%BA-%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D1%82%D1%8C-%D0%B8-%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C-%D1%82%D0%BE%D1%87%D0%BA%D1%83-%D0%B8%D0%BD%D1%82%D0%B5%D0%B3%D1%80%D0%B0%D1%86%D0%B8%D0%B8).
2. [Зарегистрируйтесь](https://app.metabot24.com/register) на платформе Metabot, подтвердите почту, [авторизуйтесь](https://app.metabot24.com/login) и создайте чат-бот.
3. Используйте готовый общий плагин или создайте плагин для своего бизнеса и скопируйте в него наш исходный код. Инструкции для обоих вариантов будут указаны ниже.
4. Создайте в чат-боте системный атрибут **mindbox.secretKey.&lt;Системное имя точки интеграции&gt;** и сохраните в него секретный ключ (токен) авторизации API запросов к Mindbox. 
    - Если у вас планируется несколько точек интеграции в чат-боте, то нужно задать свой ключ для каждой из них. Например, так: 
        - Mindbox.SecretKey.MyBusiness.Chatbot - атрибута с ключом для точки MyBusiness.Chatbot
        - Mindbox.SecretKey.MyBusiness.Chatbot.Shop - атрибута с ключом для точки MyBusiness.Chatbot.Shop
    - Внимание! Называйте атрибуты в точности и с учетом регистра именно так, как они указаны в Mindbox.
5. Реализуйте диалоговый сценарий (скрипт) с опросом данных пользователя (например, имя, фамилия, email адрес, телефон и прочее).
6. В конце диалога, обязательно (!) запросите согласие пользователя на обработку персональных данных и пришлите ссылку на положение о конфиденциальности компании. Если подписываете на маркетинговую рассылку, то дополнительно запросите согласие. Храните согласие в атрибуте лида вашего чат-бота на случай, если это понадобится юридической службе вашей компании.
7. Выберите универсальный или упрощенный метод для генерации запроса к Mindbox (смотрите детали ниже) и скопируйте код примера к себе в скрипт.
8. Кастомизируйте код интеграции под свои задачи. Если у вас возникнут затруднения, обратитесь за помощью <span style="text-align: center;">в [Телеграм-чат](https://t.me/metabot_chatroom) cообщества</span> или [поддержку Metabot](https://app.metabot24.com/help)<span style="text-align: center;">. </span>

### Вызов в диалоге

Вы можете использовать одну из двух функций на ваш выбор. Первая - универсальная - принимает тело JSON запроса в качестве одного из параметров, вторая - параметризованная - принимает поля в качестве параметров и формируем тело запроса JSON с помощью первой функции.

#### Способ 1. Универсальная (JSON) функция

Первый способ использования плагина в диалоге — с помощью универсальной функции **callMindboxEndpoint()**. Вы сами формируете тело запроса в формате JSON и передаете его в функцию.

Пример использования универсального (JSON) метода:

```JavaScript
let email = lead.getAttr("email")
let phone = lead.getAttr("phone")
let lastName = lead.getAttr("lastName")
let firstName = lead.getAttr("firstName")
let endpointId = "<Идентификатор точки интеграции>"
let operation = "<Системное имя операции>"

let jsonBody = { "customer": {
    "email": email,
    "mobilePhone": phone,
    "lastName": lastName,
    "firstName": firstName,    
    "customFields": {
       "AdCommunicationAgreement": true,
       "PersonalDataAgreement": true
    },
    "subscriptions": [
      {
        "brand": "<Системное имя бренда подписки клиента>",
        "pointOfContact": "<Системное имя канала подписки: Email, SMS, Viber, Webpush, Mobilepush>",
        "topic": "<Внешний идентификатор тематики подписки>"
      }
    ]
  },
  "pointOfContact": "<Внешний идентификатор точки контакта>"
 }

// Подключаем сниппет кода из плагина Mindbox
snippet('Common.Mindbox.Operations')

// Вызываем точку интеграциии Mindbox и передаем нужный нам JSON
callMindboxEndpoint(endpointId, operation, jsonBody)
```

Функция требует передачи трех переменных **в строгом порядке**:

<table border="1" id="bkmrk-endpointid-%D0%A3%D0%BD%D0%B8%D0%BA%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B" style="border-collapse: collapse; width: 100%;"><tbody><tr><td style="width: 21.7891%;">endpointId</td><td style="width: 78.209%;">Уникальный идентификатор интеграции.

Не забудьте, что каждому endpointId соответствует свой secretKey, который нужно сохранить в системную атрибуту бота.

Интеграции настраивается в системе Mindbox.

</td></tr><tr><td style="width: 21.7891%;">operation</td><td style="width: 78.209%;">Название операции в Mindbox. Каждому типу действия в Mindbox соответствует своя операция.  
Список операций настраивается в системе Mindbox.</td></tr><tr><td style="width: 21.7891%;">jsonBody</td><td style="width: 78.209%;">Тело запроса в формате JSON.  
Формат тела запроса, различается в зависимости от типа операции.</td></tr></tbody></table>

#### Способ 2. Альтернативная (параметризованная) функция

Второй способ использования плагина — с помощью альтернативной функции **callMindboxEndpointAlt()**, в которую вы передаете параметры запроса, а функция внутри себя формирует тело запроса в формате JSON и затем вызывает описанную выше универсальную функцию.

Пример использования альтернативного (параметризованного) метода:

```JavaScript
let email = lead.getAttr("email")
let phone = lead.getAttr("phone")
let lastName = lead.getAttr("lastName")
let firstName = lead.getAttr("firstName")
let endpointId = "<Идентификатор точки интеграции>"
let operation = "<Системное имя операции>"

// Подключаем сниппет кода из плагина Mindbox
snippet('Common.Mindbox.Operations')

// Вызываем точку интеграциии Mindbox и передаем нужные нам параметры
callMindboxEndpointAlt(endpointId, operation, 
                       email, phone, lastName, firstName, 
                       subscriptionTopic, pointOfContact)
```

Функция требует передачи нескольких переменных **в строгом порядке**. Добавляйте и удаляйте параметры по вкусу ;) но при этом не забудьте поменять в плагине код формирования JSON.

<table border="1" id="bkmrk-endpointid-%D0%A3%D0%BD%D0%B8%D0%BA%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B-0" style="border-collapse: collapse; width: 100%; height: 372.424px;"><tbody><tr style="height: 102.519px;"><td style="width: 21.7548%; height: 102.519px;">endpointId</td><td style="width: 78.2433%; height: 102.519px;">Уникальный идентификатор интеграции.

Не забудьте, что каждому endpointId соответствует свой secretKey, который нужно сохранить в системную атрибуту бота.

Интеграции настраивается в системе Mindbox.

</td></tr><tr style="height: 63.3428px;"><td style="width: 21.7548%; height: 63.3428px;">operation</td><td style="width: 78.2433%; height: 63.3428px;">Название операции в Mindbox. Каждому типу действия в Mindbox соответствует своя операция.  
Список операций настраивается в системе Mindbox.</td></tr><tr style="height: 29.7633px;"><td style="width: 21.7548%; height: 29.7633px;">email</td><td style="width: 78.2433%; height: 29.7633px;">Email пользователя</td></tr><tr style="height: 35.3598px;"><td style="width: 21.7548%; height: 35.3598px;">phone</td><td style="width: 78.2433%; height: 35.3598px;">Мобильный телефон

</td></tr><tr style="height: 35.3598px;"><td style="width: 21.7548%; height: 35.3598px;">lastName</td><td style="width: 78.2433%; height: 35.3598px;">Фамилия

</td></tr><tr style="height: 35.3598px;"><td style="width: 21.7548%; height: 35.3598px;">firstName</td><td style="width: 78.2433%; height: 35.3598px;">Имя

</td></tr><tr style="height: 35.3598px;"><td style="width: 21.7548%; height: 35.3598px;">subscriptionTopic</td><td style="width: 78.2433%; height: 35.3598px;">Внешний идентификатор тематики подписки. Настраивается в Mindbox.

</td></tr><tr style="height: 35.3598px;"><td style="width: 21.7548%; height: 35.3598px;">pointOfContact</td><td style="width: 78.2433%; height: 35.3598px;">Внешний идентификатор точки контакта. Настраивается в Mindbox.

</td></tr></tbody></table>

#### Варианты подключения плагина

Вы можете использовать один из двух вариантов: воспользоваться общим плагином без модификаций кода или же скопировать код плагина к себе и модифицировать код.

Вы можете использовать общий плагин и вызвать скрипт "Вызов операции" в точности следуя примерам выше. В этом случае, для использования общего плагина используйте вызов сниппета из коллекции общих плагинов **Common**:

```JavaScript
snippet('Common.Mindbox.Operations');
```

Либо, вы можете создать свой плагин в вашем бизнесе, скопировать наш и изменив код под себя, для этого:

1. Cоздайте плагин в вашем бизнесе и назовите его **Mindbox**.
2. Создайте в плагине скрипт и назовите его **Operations**.
3. Скопируйте код, размещенный ниже, в код нового скрипта.

В этому случае, в примерах, указанных выше, замените вызов общего сниппета на ваш собственный:

```JavaScript
snippet('Business.Mindbox.Operations');  
```

В обоих случаях, перед вызовом сниппета требуется объявить все необходимые переменные и передать им соответствующие значения.

При успешном запросе во вкладке "Клиенты" в Mindbox будет создан клиент с переданными из чат-бота данными о нем:

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2022-11/scaled-1680-/qtyPh5FE7Vf8raIw-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2022-11/qtyPh5FE7Vf8raIw-image.png)

Так же, во вкладке "Действия" вы сможете найти историю выполненной операции:

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2022-11/scaled-1680-/mZvbMZeFukIquXcl-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2022-11/mZvbMZeFukIquXcl-image.png)

#### Обработка ошибок

В случае успешного выполнения оба метода возвращают **true**. В случае ошибки оба метода возвращают **false**, а в атрибутах лида и бота вы сможете найти информацию о последней ошибке. Название атрибуты и описание указано в таблице ниже.

<table border="1" id="bkmrk-%D0%A5%D1%80%D0%B0%D0%BD%D0%B8%D0%BB%D0%B8%D1%89%D0%B5%C2%A0-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-" style="border-collapse: collapse; width: 100%; height: 148.817px;"><tbody><tr style="height: 29.7633px; background-color: #ced4d9;"><td style="width: 14.5856%; height: 29.7633px;">Хранилище </td><td style="width: 32.267%; height: 29.7633px;">Название атрибута</td><td style="width: 53.1455%;">Описание</td></tr><tr style="height: 29.7633px;"><td style="width: 14.5856%; height: 29.7633px;">lead</td><td rowspan="2" style="width: 32.267%;"><span class="hljs-string">plugin.mindbox.lastError.code</span>  
<span class="hljs-string">  
</span></td><td rowspan="2" style="width: 53.1455%;">В случае ошибки, будет содержать код ошибки плагина. Смотрите таблицу ошибок ниже.  
</td></tr><tr><td style="width: 14.5856%;">bot</td></tr><tr style="height: 29.7633px;"><td style="width: 14.5856%; height: 29.7633px;">lead</td><td rowspan="2" style="width: 32.267%;"><span class="hljs-string">plugin.mindbox.lastError.message</span>  
<span class="hljs-string">  
</span></td><td rowspan="2" style="width: 53.1455%;"><span class="hljs-string">В случае ошибки, будет содержать сообщение об ошибке плагина. Смотрите таблицу ошибок ниже.</span><span class="hljs-string">  
</span></td></tr><tr><td style="width: 14.5856%;">bot</td></tr></tbody></table>

##### Список ошибок

В случае ошибки плагин может вернуть одну из следующих ошибок, а также вернет сообщение об ошибке. В таблице ниже представлены все возможные ошибки плагина и рекомендации, что вы можете сделать в каждом конкретном случае.

<table border="1" id="bkmrk-%D0%9A%D0%BE%D0%B4-%D0%BE%D1%88%D0%B8%D0%B1%D0%BA%D0%B8-%D0%A1%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B5" style="border-collapse: collapse; width: 100%; height: 149.356px;"><tbody><tr style="height: 29.7633px; background-color: #c2e0f4;"><td style="width: 12.1135%; height: 29.7633px;">Код ошибки</td><td style="width: 24.225%;"><span class="hljs-string">Сообщение об ошибке</span></td><td style="width: 63.6596%; height: 29.7633px;">Рекомендации</td></tr><tr style="height: 44.9148px;"><td style="width: 12.1135%; height: 44.9148px;">1</td><td style="width: 24.225%;">Ошибка вызова Mindbox API ({код ошибки}).

</td><td style="width: 63.6596%; height: 44.9148px;">Плагин вернет код ошибки Mindbox API. Поскольку чат-бот не запоминает детали вызова API потому что вызовы асинхронны (async), при получении данной ошибки детали смотрите на стороне Mindbox.

Все возможные коды ошибок Mindbox API cмотрите в документации к Mindbox: [https://developers.mindbox.ru/docs/error\_processing](https://developers.mindbox.ru/docs/error_processing)

Подробнее про вызов операции через Mindbox API, который применяется в этом плагине, смотрите по ссылке: [https://developers.mindbox.ru/docs/v3](https://developers.mindbox.ru/docs/v3)

</td></tr><tr style="height: 44.9148px;"><td style="width: 12.1135%; height: 44.9148px;">2</td><td style="width: 24.225%;">Не задан ключ интеграции {имя атрибута} в атрибутах бота.

</td><td style="width: 63.6596%;"><span class="hljs-string">Проверьте не ошиблись ли вы в регистре символов или названии.</span>

</td></tr><tr style="height: 29.7633px;"><td style="width: 12.1135%; height: 29.7633px;">3</td><td style="width: 24.225%;">Не заданы обязательные параметры параметризованного метода.

</td><td style="width: 63.6596%;">Проверьте не пытаетесь ли вы передать пустые данные в Mindbox в одном из полей (смотрите поля в атрибутах лида). А если параметр допустимо передавать пустым, удалите соответствующую валидацию в коде в callMindboxEndpointAlt().

</td></tr></tbody></table>

### Исходный код плагина

Cкопируйте указанный ниже код в скрипт Operations вашего плагина Mindbox и измените как вам требуется.

```JavaScript
/**
 * Универсальная функция для регистрации операции в Mindbox через точку интеграции.
 * Используется явное указание тела запроса в формате JSON.
 * @param {string} endpointID - Идентификатор точки интеграции в Mindbox
 * @param {string} operation - Системное имя операции
 * @param {object} jsonBody - Тело запроса (берется из настроек операции в Mindbox)
 * @returns {bool} - Результат выполнения функции: успешно (true), проблемы (false).
 */
function callMindboxEndpoint(endpointId, operation, jsonBody) {
  // Считываем ключ авторизации из аттрибуты бота
  let secretKey = bot.getAttr('mindbox.secretKey.'+ endpointId)
  // Если ключ не настроен
  if (!secretKey) {
    outputError(endpointId, operation, '2', 'Не задан ключ интеграции plugin.Mindbox.secretKey в атрибутах чат-бота.')
    return false
  }
  
  // Задаем заголовок запроса
  api.setHeaders({'authorization':'Mindbox secretKey="' + secretKey + '"'})
 // Задаем URL запроса (используем асинхронный 'async' метод)
  let url = "https://api.mindbox.ru/v3/operations/async?endpointId=" + endpointId + "&operation=" + operation

  // Выполняем запрос с помощью метода POST и запоминаем результат
  let jsonResponse = api.postJson(url, jsonBody) 
  let jsonResponseCode = api.getLastResponseCode()

  // Возникла ошибка
  if (jsonResponseCode != 200) {
    outputError(endpointId, operation, '1', 'Проблема вызова Mindbox API. Детали смотрите в Mindbox.')
    return false // Выполнено с проблемами
  }
  
  return true // Все ок
}

/**
 * Альтернативная (параметризованная) функция для регистрации операции в Mindbox, который подготавливает тело запроса.
 * Адаптируйте эту функцию под свой проект или создайте копию.
 * @param {string} endpointID - Идентификатор точки интеграции в Mindbox
 * @param {string} operation - Системное имя операции
 * @param {string} email - Email
 * @param {string} phone - Мобильный телефон
 * @param {string} lastName - Фамилия
 * @param {string} firstName - Имя
 * @param {string} subscriptionTopic - Внешний идентификатор тематики подписки
 * @param {string} pointOfContact - Внешний идентификатор точки контакта
 * @returns {bool} - Результат выполнения функции: успешно (true), проблемы (false).
 */
function callMindboxEndpointAlt(endpointId, operation, email, phone, lastName, firstName, subscriptionTopic, pointOfContact) {
  // Если передали пустые значения
  if (isStrEmpty(endpointId) || 
      isStrEmpty(operation) || 
      isStrEmpty(email) || 
      isStrEmpty(phone) || 
      isStrEmpty(lastName) || 
      isStrEmpty(firstName) || 
      isStrEmpty(subscriptionTopic) || 
      isStrEmpty(pointOfContact))
  {
    outputError('3', 'Не заданы обязательные параметры параметризованного метода.')
    return false
  }
  
  // Формируем тело запроса в JSON (в вашем проекте запрос может отличаться)
  let jsonBody = { 
    "customer": {
      "email": email,
      "mobilePhone": phone,
      "lastName": lastName,
      "firstName": firstName,    
      "customFields": {
         "AdCommunicationAgreement": true, // Согласие на рассылку
         "PersonalDataAgreement": true     // Согласие на обработку персональных данных
      },
      "subscriptions": [
        {
          "brand": "<Системное имя бренда подписки клиента>",
          "pointOfContact": "<Системное имя канала подписки: Email, SMS, Viber, Webpush, Mobilepush>",
          "topic": subscriptionTopic
        }
      ]
    },
    "pointOfContact": pointOfContact
  }
  
  // Вызываем универсальную функцию и возвращем результат
  return callMindboxEndpoint(endpointId, operation, jsonBody)
}

/**
 * Вспомогательная функция, которая сохраняет сведения об ошибке в атрибутах лида и атрибутах бота.
 * @param {string} endpointId - Точка интеграции
 * @param {string} operation - Операция
 * @param {string} code - Код ошибки
 * @param {string} message - Сообщение об ошибке
 */
function outputError(endpointId, operation, code, message) {
  // Добавляем в конце сообщения доп. инфу.
  message = message + ' (точка=' + endpointId + ', операция=' + operation + ')'
  lead.setAttr("plugin.Mindbox.lastError.Code", code)
  lead.setAttr("plugin.Mindbox.lastError.Message", message)  
  bot.setAttr("plugin.Mindbox.lastError.Code", code)
  bot.setAttr("plugin.Mindbox.lastError.Message", message)   
}

/**
 * Вспомогательная функцию, которая проверяет является ли строка пустой.
 * @param {string} code - Код ошибки
 * @returns {bool} - если строка пустая (true), иначе (false).
 */
function isStrEmpty(str) {
  return (typeof str === 'string' && str.trim().length === 0) ? true : false
}
```

<div class="heading-text" id="bkmrk--3"></div>

# Диалоговое путешествие (Dialog Journey)

<table border="1" id="bkmrk-%D0%90%D0%B2%D1%82%D0%BE%D1%80-%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%B0-%D0%9F%D0%B5%D1%82%D1%80%D0%BE%D0%B2" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; width: 102.469%; height: 158.941px;"><tbody><tr style="height: 29.7917px;"><td style="width: 38.2087%; height: 29.7917px;">**Название плагина**</td><td style="width: 61.7894%; height: 29.7917px;">Диалоговое путешествие (Dialog Journey)</td></tr><tr style="height: 29.7917px;"><td style="width: 38.2087%; height: 29.7917px;">**Разработчик**</td><td style="width: 61.7894%; height: 29.7917px;">Официальные плагины от Metabot</td></tr><tr style="height: 39.7746px;"><td class="align-left" style="width: 38.2087%; height: 39.7746px;">**Авторы**</td><td style="width: 61.7894%; height: 39.7746px;">Гарашко Артем Юрьевич ([artem@metabot.org](mailto:artem@metabot.rg))

</td></tr><tr style="height: 29.7917px;"><td class="align-left" style="width: 38.2087%; height: 29.7917px;">**Дата создания**</td><td style="width: 61.7894%; height: 29.7917px;">04 Января 2023</td></tr><tr style="height: 29.7917px;"><td class="align-left" style="width: 38.2087%; height: 29.7917px;">**Последняя дата обновления**</td><td style="width: 61.7894%; height: 29.7917px;">06 Января 2023</td></tr></tbody></table>

### Описание

<div id="bkmrk-%D0%9F%D0%BB%D0%B0%D1%82%D1%84%D0%BE%D1%80%D0%BC%D0%B0-metabot-%E2%80%94-">Платформа Metabot — это платформа автоматизации коммуникаций. Плагин **Dialog Journey (DJ)** для платформы Metabot позволяет интегрировать в ваш чат-бот, разработанный на Metabot или на любой другой бот-платформе, поддерживающей вызовы и приемы API веб-хуков, возможность проектирования, отслеживания и визуализации клиентских путешествий, также называемых клиентскими путями (customer journeys).</div><div id="bkmrk-">  
</div><div id="bkmrk-%D0%98%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D1%8F-%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD-%D0%B8-%D0%BF">Используя плагин и платформу Metabot, вы можете создавать диалоговые стратегии и путешествия клиентов, которые помогут вашим клиентам получать пользу, а вашей компании достигать поставленных целей, собирать аналитику о ходе продвижения клиентов по диалогам в чат-боте, отслеживать и визуализировать карты их путей.</div><div id="bkmrk--0">  
</div>Плагин позволит организовать развитие вашего чат-бота таким образом, чтобы все разрозненные диалоги объединялись в единую **коммуникационную стратегию** компании, представленную в виде путешествий (journeys), разбитых на фазы (phases) с целями (goals), предоставляемой пользой (values) и измеряемыми показателями (metrics). Более подробное описание концепции и устройства плагина, а также предлагаемой методологии маркетинговой стратегии смотрите ниже.

<div id="bkmrk--1"></div>### Пример

Пример работы чат-бота с интегрированным плагином можете посмотреть перейдя по <span style="text-decoration: underline;">ссылке</span>.

### Настройка 

Для интеграции плагина DJ с вашим чат-ботом вам необходимо сделать следующее:

1. Создать кастомные таблицы и заполнить их, согласно схеме базы данных, опубликованной в разделе **Справочники**.
2. Спроектировать путешествия для ваших клиентских сегментов и настроить справочники, описания назначения которых смотрите ниже. 
    1. Классы персон.
    2. Подклассы персоны.
    3. Путешествия.
    4. Фазы путешествия.
    5. Польза.
    6. Цели.
    7. Показатели.
    8. Активности.
3. Скачать готовый шаблон чат-бота, в который уже интегрирован плагин DJ, адаптировать его код и структуру под ваши задачи в своем чат-боте. 
    1. <span style="text-decoration: underline;">Ссылка.</span>
4. Ознакомиться с примерами когда вызова методов DJ из вашего чат-бота в разделе **JS команды**.
5. Ознакомиться с инструкций как пользоваться аналитикой и отчетами в разделе **Аналитика и отчеты**.

Если разработка чат-бота ведется на сторонней платформе, в дополнение к шагам, описанным выше, вам также необходимо настроить точки интеграции от вашего чат-бота, созданного на сторонней платформе, к чат-боту, созданному на Metabot, работающему как внешнее хранилище данных для следующих целей:

1. Чтобы ваш чат-бот мог передавать в базу данных Мetabot все нужные вам события для аналитики, например, сообщать, что пользователь достиг цели или перешел к следующей фазе путешествия.
2. Чтобы ваш чат-бот мог запрашивать в базе данных Metabot информацию о состоянии путешествия пользователя, например, проверять достиг ли пользователь цели, были ли предоставлена польза, узнавать фазу путешествия на которой находится пользователь и так далее.

Инструкцию о том, как подключить Dialog Journeys к чат-боту (а может и не только к чат-боту), создаваемому на сторонней платформе, смотрите в разделе **Интеграция со сторонними системами**.

### Методология 

#### Предисловие

Представленное решение — это уникальный синтез нашего собственного практического опыта и изысканий с идеями ведущих мировых теоретиков из сферы маркетинга, управления бизнесом и инноваций.

Возможно модель подойдет к вашей бизнес-практике идеально «как есть». Мы постарались сделать модель простой и в то же время достаточно гибкой — принципы, лежащие в ее основе, универсальны и действенны.

Возможно вы не согласитесь с представленной моделью работы с клиентами и решите сделать по-другому, либо захотите расширить или откорректировать модель. Вы вправе это сделать. Если вам потребуется разработка уникального плагина под ваше видение, обращайтесь в наш отдел разработки и мы сделаем решение «под ключ».

Методология маркетинга, на которой строится работа данного плагина, стоит на нескольких китах. Во-первых, это решение для современного маркетинга для «экономики связей» или «экономики подключения» (Connection Economy). Подробнее об этой концепции смотрите <span style="text-decoration: underline;">здесь</span>.

Во-вторых, это решение строится на **маркетинге доверия**. Подробнее об этой концепции смотрите соответствующие обучающие материалы и экспертов. Например, <span style="text-decoration: underline;">здесь</span>.

#### Справочники

##### Классы персон

Проектирование начинается с описания клиентских сегментов — мы их называем **Классы персон**. Подобно, RPG играм, опишите все классы, с которыми имеет дело ваш бизнес.

- Пример из сказочный: воин, лучник, волшебник;
- Пример из жизни: строитель, заказчик, партнер.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/Sh9KjRxQgzzGdChX-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/Sh9KjRxQgzzGdChX-image.png)

##### Подклассы персон

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

- Пример для класса лучника: стрелок, снайпер, охотник;
- Пример для класса строителя: индивидуальный строитель, профессиональная бригада, крупный застройщик.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/WmZdMtuyN11JGWvS-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/WmZdMtuyN11JGWvS-image.png)

##### Путешествия

Создайте все необходимые **Путешествия**, как общие для всех классов и подклассов, так и индивидуальные для каждого сегмента.

Если вы впервые сталкивайтесь с разработкой чат-ботов, мы рекомендуем не усложнять и начать автоматизацию с одного наиболее популярного и критичного путешествия, например, с вовлечения и онбординга нового пользователя.

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

Примеры путешествий:

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

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/ROd2jqKsKenx4LLQ-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/ROd2jqKsKenx4LLQ-image.png)

<p class="callout info">Плагин работает так, что запоминает один единственный раз, когда пользователь пустился в путешествие. Плагин на текущий момент не поддерживает повторные путешествия по одному и тому же путешествию. Если вам необходимо заново отправить пользователя в повторное путешествие, для этого создайте новое путешествие или очистите старые данные по путешествию в базе данных.</p>

##### Цели 

Следующим шагом, который на самом деле надо делать одновременно с предыдущим, мы рекомендуем заполнить **Цели** для путешествий. Мы же с вами занимаемся бизнесом и не хотим создавать бесцельные путешествия, верно?

Пример целей для онбординга партнеров:

- Регистрация в партнерской программе;
- Сертификация партнера.

Пример целей для обучения стрельбе из луков:

- Обучить лучника;
- Продать лук и стрелы;
- Продать билеты на соревнование по стрельбе.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/uUd9oJr3ecQ3Rcty-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/uUd9oJr3ecQ3Rcty-image.png)

<p class="callout info">Плагин работает так, что позволяет записать достижения цели по каждому путешествию для каждого пользователя только один единственный раз. Если в чат-боте пользователь несколько раз пройдет по сценарию, то повторные достижения одной и той же цели будут проигнорированы.</p>

##### Фазы путешествия

Разбейте каждое путешествие на **Фазы**. Мы сознательно используем слово «фазы», а не «этапы» или «шаги», потому что оно нам больше нравится — ведь переход от фазы к фазе т.е. так называемый «фазовый переход» подразумевает некую качественно новую форму, например, лед при нагревании превращается в воду, а при еще большем нагревании в пар. Мы же с вами хотим строить диалоги с нашей аудиторией так, чтобы каждый раз выходить на новый уровень взаимоотношений, верно?

Пример фаз для онбординга нового партнера:

- Вовлечение;
- Знакомство;
- Сертификация;
- Первая транзакция.

Пример фаз для учебы стрельбе из лука:

- Изучает основы по стрельбе и безопасности;
- Учится делать DIY лук и стрелы;
- Учится стрелять;
- Сдать экзамен по стрельбе;
- Записать на соревнование по стрельбе.

Согласитесь, что каждая из фаз, приведенных в примерах выше, символизирует качественно новое состояние, дойдя до которого, наш пользователь, уже не сможет вернуться назад и забыть все то, что было до этого?

Наша цель при проектировании и разработке чат-ботов и авто-воронок заключается в том, чтобы выстраивать диалоговые коммуникации таким образом, чтобы они помогали пользователю двигаться от фазы к фазе, получая пользу, и тем самым доходить до завершения путешествия и помогая нам достигать бизнес-цели. Как именно строить такие коммуникации мы разберем ниже.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/PthMoEOsvxnffnIV-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/PthMoEOsvxnffnIV-image.png)

<p class="callout info">Плагин работает так, что запоминает один единственный раз, когда пользователь завершал одну фазу путешествия и начинал новую. Плагин запоминает дату и время фазовых переходов, чтобы по ним строить аналитику. Если пользователь пришел в сценарий в чат-боте, в котором в DJ сообщается о смене фазы, то плагин проигнорирует повторные команды. </p>

##### Польза 

Теперь самое интересное. Мы расписали наши путешествия, фазы и цели. Теперь давайте подумаем о том, как же мы будем влиять на наших пользователей, чтобы они достигали нужных нам целей?

Спланируйте **Пользу**, которую будете предоставлять вашей аудитории, на каждой фазе путешествия, чтобы выстраивать доверительные отношения и двигаться вместе с ней к целям.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/Js3bL8UtCGl2GxTO-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/Js3bL8UtCGl2GxTO-image.png)

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

- **Эмпатия**
    - Давайте мы будем пытаться понять кто перед нами, а не бомбардировать людей не актуальной для них информацией;
    - Давайте понимать на каком этапе принятий решения о покупке (buyer's journey) находится человек, чтобы давать именно то, что нужно сейчас;
    - Давайте стараться учитывать чувства и эмоции людей, чтобы отвечать корректно контексту ситуации;
    - Здорово, что чат-боты помогают все это реализовать, ведь чат-бот это диалог, в котором можно задавать вопросы и запоминать ответы, а благодаря технологии распознавания естественного языка (NLP) можно понять намерение пользователя по свободному вводу и даже распознать эмоции;
    - Пример для строительной сферы: в самом начале коммуникации с целевой аудиторией, задайте 2-3 квалифицирующих вопроса, которые помогут определить к какому сегменту (классу и подклассу) относится человек. Также, вы можете узнать квалификацию строителя (профессионал или новичок), чтобы в последствии вести каждый сегмент по своему уникальному пути.
    - Пример для сказочной истории: аналогично, cоздайте квалификационную анкету, в которой будет несколько вопросов, которые помогут вообще понять кто перед нами. Вы не не захотите "впаривать" стрельбу из лука волшебнику 100500 уровня, который обидится и превратит вас в лягушку? =)
- **Персонализация**
    - Раз мы можем говорить с пользователем и задавать интересующие нам вопросы, так давайте использовать полученную информацию для построения персонализированных диалогов. А если интегрировать чат-бот с корпоративными информационными системами, например, с E-Commerce веб-сайтом, где хранится история покупок пользователя, то можно строить еще более полезные персонализированные диалоги и предложения.
    - Пример: как минимум, можно иногда обращаться по имени. Конечно, это не заставит пользователя прийти в восторг и сделать покупку, на, как минимум, повлияет на общее восприятие вашего бренда и позволит заработать очки доверия. Ведь мы же строим долгосрочные отношения на доверии, а значит должны всегда заботиться о целостности и последовательности наших коммуникаций.
    - Пример из строительной отрасли: узнав сегмент, опыт строителя и регион, вы можете сделать персональное предложение, например, предложить партнерам присылать заявки на строительный заказы из этого региона, предложить горячие скидки на продукцию любимого бренда, пройти обучение для новичков и так далее.
    - Пример для сказочной истории: узнав, что перед нами маг, мы можем предложить ему магазин для магов от наших партнеров или просто выпить чашечку кофе.
- **Такт и ритм**
    - Мы настоятельно рекомендуем пользоваться житейским здравым смыслом и бизнес этикой при программировании автоматических коммуникаций.
    - Также как и в реальных отношениях в нашей жизни, давайте cоблюдать чувство такта и ритма в цифровых отношениях, которые автоматизируем. Людям не понравится, если мы пишем им, как назойливая муха, с поводом или без повода. Также, люди могут про нас начитать забывать и контакт потом восстановить будет сложнее, если мы совсем перестанем общаться. Если же мы будем присылать информацию, которая не релеванта, не принимая обратную связь и не корректируя коммуникацию, то люди могут отправить нас "в баню". И так далее.
    - Пример из строительной отрасли: когда мы узнаем темп обучения, который удобен коллеге-строителю для повышения своей квалификации, мы можем начать присылать ему обучающие материалы прямо в чат-бот так часто как будет удобно: раз в день, неделю, месяц.
    - Пример из сказочной отрасли: узнав, что перед нами опытный следопыт, который отправляется в охотничьи вылазки раз в месяц, мы можем отправлять ему информацию о новинках прямо на кануне очередного похода, о котором можем узнать, спросив об этом.
- **Доверие**  
    
    - Доверие клиента или партнера — это самое главное конкурентное преимущество и один из самых главных активов. Никто не свяжет свою жизнь с человеком, в котором не уверен. То же самое происходит в бизнесе. Теряя доверие к бренду или компании, вы уходите к другим, верно? Ваши клиенты делают так же.
    - Завоевание доверия это сложный и длительный процесс, требующим особого подхода. Большое доверие увеличивает продажи и доходы компании, а потеря доверия увеличивает расходы на рекламу и маркетинг. Доверие зарабатывается по крупицам, а потерять его можно в один момент.
    - Конечно, доверие к бренду или компании складывается из многих факторов, на которые мы не можем влиять чат-ботом, например, из качества продукции. Если продукция плохая, чат-бот вряд ли сильно поможет в выстраивании доверия, какие бы красиво он не говорил.
    - Однако, если в компании порядок с продукций и сервисом, то создание захватывающего, полезного и удобного клиентского опыта (CX) в виде бесшовного диалога в чат-боте, интегрированного в бизнес-процессы и информационные системы предприятия, может стать тем самым ключом к конкурентному преимуществу, который изменит баланс весов в пользу вашей компании.
    - Пример доверия: хорошо понимать своих клиентов, проектировать правильные путешествия, которые дают пользу, помнить о долгосрочности диалогов и отношений, выстроить полезный сервис в чат-боте, которым хочется пользоваться и рассказать другим.
    - Пример потери доверия: игнорировать запросы клиентов, не решать пожарные ситуации, долго строить отношения по стратегии, а потом начать рассылать спам и прочее.
- **Щедрость**  
    
    - Тоже не менее важный принцип, как и доверие. В эпоху перепроизводства и высокой конкурентности (когда в каждой категории на полке десятки и сотни продуктов), щедрость это то, что позволяет выстраивать доверительные отношения.
    - Как и в жизни, так и в бизнесе, никто не захочет дружить с теми кто только «берет» и не готов «отдавать».
    - Что означает щедрость в применении к чат-ботам: полезный контент, которым вы можете делиться; полезные вебинары; выставки; сервисы и так далее.

<p class="callout info">Аналогично целям, предоставление пользы фиксируется один единственный раз и повторные выдачи той же самой пользы не фиксируются в базе данных, поэтому вы можете не опасаться за то, что пользователь несколько раз обратился за одним и тем же контентом — это не собьет статистику. Предоставление пользы будет отслежено только самый первый раз. </p>

##### Показатели

Спланируйте **Показатели** (или метрики), которые вам необходимо измерять.

Пример из жизни:

- Вы можете завести метрику, которая отображает % потребления контента, чтобы оценивать степень "созревания" пользователя;
- Индекс NPS (удовлетворенность компанией);
- Настроение, например, вы можете периодически опрашивать как у ваших пользователей дела ;
- Степень осведомленности о продукции компании (от 0 до 100).

Сказочный пример:

- Количество купленных стрел;
- Количество сломанных луков.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/xS7cTgNxYIH7N3xH-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/xS7cTgNxYIH7N3xH-image.png)

<p class="callout info">Показатели, в отличие от Целей и Пользы, можно измерять сколько угодно раз и в любое время. То есть, если с течением времени показатель меняется, у вас будет вся история изменений.</p>

<p class="callout info">В текущей версии плагина показатели закрепляются за путешествием и фазой во время которых они были собраны. В будущих релизах планируется сделать поддержку сбора общих показателей, не относящихся к конкретному путешествию или фазе. </p>

##### Активности

**Активности** — это события, которые происходят в ходе путешествия или действия, которые совершает пользователя, во время путешествия.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/aaeDllQs2ESRs41o-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/aaeDllQs2ESRs41o-image.png)

Существует 7 основных событий, которые позволяют управлять ходом путешествия и отслеживать его хронологию:

<table border="1" id="bkmrk-id-%D0%9A%D0%BE%D0%B4-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%9F%D0%BE%D1%8F%D1%81" style="border-collapse: collapse; width: 109.012%; height: 540.583px;"><tbody><tr style="height: 29.7917px; background-color: #c2e0f4;"><td style="width: 8.03461%; height: 29.7917px;">ID</td><td style="width: 20.2719%; height: 29.7917px;">Код</td><td style="width: 28.6925%; height: 29.7917px;">Название</td><td style="width: 42.8774%; height: 29.7917px;">Пояснение</td></tr><tr style="height: 80.1667px;"><td style="width: 8.03461%; height: 80.1667px;">1</td><td style="width: 20.2719%; height: 80.1667px;">journeyStarted</td><td style="width: 28.6925%; height: 80.1667px;">Путешествие начато</td><td style="width: 42.8774%; height: 80.1667px;">Используется в самом начале путешествия, когда стало понятно, что пользователь "отправился в путешествие".</td></tr><tr style="height: 80.1667px;"><td style="width: 8.03461%; height: 80.1667px;">2</td><td style="width: 20.2719%; height: 80.1667px;">phaseStarted </td><td style="width: 28.6925%; height: 80.1667px;">Фаза начата</td><td style="width: 42.8774%; height: 80.1667px;">Используется при запуске следующей фазы, а также автоматически в начале путешествия.</td></tr><tr style="height: 80.1667px;"><td style="width: 8.03461%; height: 80.1667px;">3</td><td style="width: 20.2719%; height: 80.1667px;">phaseCompleted </td><td style="width: 28.6925%; height: 80.1667px;">Фаза завершена </td><td style="width: 42.8774%; height: 80.1667px;">Используется при запуске следующей фазы, завершая предыдущую, и при завершении всего путешествия.</td></tr><tr style="height: 113.75px;"><td style="width: 8.03461%; height: 113.75px;">4</td><td style="width: 20.2719%; height: 113.75px;">phaseInterrupted </td><td style="width: 28.6925%; height: 113.75px;">Фаза прервана</td><td style="width: 42.8774%; height: 113.75px;">Используется при прерывании фазы, которое происходит либо при прерывании всего путешествия, либо когда по каким-то причинам в нам нужно будет прервать фазу.</td></tr><tr style="height: 63.375px;"><td style="width: 8.03461%; height: 63.375px;">5</td><td style="width: 20.2719%; height: 63.375px;">phaseSkipped </td><td style="width: 28.6925%; height: 63.375px;">Фаза пропущена </td><td style="width: 42.8774%; height: 63.375px;">Используется по каким-то причинам когда вам необходимо пропустить целую фазу.</td></tr><tr style="height: 63.375px;"><td style="width: 8.03461%; height: 63.375px;">6</td><td style="width: 20.2719%; height: 63.375px;">journeyCancelled </td><td style="width: 28.6925%; height: 63.375px;">Путешествие отменено </td><td style="width: 42.8774%; height: 63.375px;">Используется когда путешествие было отменено, например, пользователь передумал.</td></tr><tr style="height: 29.7917px;"><td style="width: 8.03461%; height: 29.7917px;">7</td><td style="width: 20.2719%; height: 29.7917px;">journeyCompleted </td><td style="width: 28.6925%; height: 29.7917px;">Путешествие завершено</td><td style="width: 42.8774%; height: 29.7917px;">Путешествие успешно завершено, пользователь дошел до победного конца.</td></tr></tbody></table>

<p class="callout info">В одном из будущих релизов планируется возможность добавлять пользовательские активности, чтобы вы могли отслеживать промежуточные шаги во время фазы, если вам это понадобится.</p>

### Журналы

Все что происходит в чат-боте сохраняется в несколько **Журналов**, а также сохраняется в **Состоянии пользователей в путешествии**. Это системные таблицы, в которых накапливаются исторические данные, которые вам не нужно трогать. На основе журналов и справочников, формируются аналитические отчеты и визуализации.

<p class="callout warning">Ни в коем случае не нарушайте целостность данных, если уже запустили трафик и идет сбор данных о путешествиях пользователей. Если у вас есть журналы с данными, а вы решите удалить или изменить данные в **Справочниках**, отдавайте себе отчет, что это может повлиять на исторические данные, ранее собранные чат-ботом, а значит и на отчеты. </p>

#### Журнал путешествий 

В этом журнале хранится история фаз и активностей, а также начала и завершения путешествий.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/8KnvxY4wbhriKAQa-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/8KnvxY4wbhriKAQa-image.png)

#### Журнал пользы

В этом журнале хранится история предоставления пользы пользователям.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/llT4NajiENhoFm9i-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/llT4NajiENhoFm9i-image.png)

#### Журнал целей

В этом журнале хранится история достижения целей.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/4hTDFc35TETwFvRD-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/4hTDFc35TETwFvRD-image.png)

#### Журнал показателей 

В этом журнале хранятся собранные метрики.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/WGX6xcTZiEFYYItg-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/WGX6xcTZiEFYYItg-image.png)

#### Состояние пользователя в путешествии

В этой таблице хранится состояние пользователя в конкретном путешествии.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-01/scaled-1680-/gphCiREYGeN217lY-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-01/gphCiREYGeN217lY-image.png)

<p class="callout warning">Ни в коем случае не трогайте эти данные на продакшене, кроме случаев отладки, иначе нарушите ход путешествия для пользователей.</p>

### JS команды

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

Список команд и примеры кода представлены в таблице ниже.

<table border="1" id="bkmrk-%E2%84%96-%D0%9D%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D0%B0-%D0%9F%D1%80" style="border-collapse: collapse; width: 100%; height: 908.73px;"><tbody><tr style="height: 29.7917px;"><td style="width: 9.76514%; height: 29.7917px;">№</td><td style="width: 21.7789%; height: 29.7917px;">Название метода</td><td style="width: 68.456%; height: 29.7917px;">Пример кода</td></tr><tr style="height: 206.021px;"><td style="width: 9.76514%; height: 206.021px;">1</td><td style="width: 21.7789%; height: 206.021px;">Начать путешествие

</td><td style="width: 68.456%; height: 206.021px;">```JavaScript
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Начинаем новое
let isNewJourneyStarted = jm.startNewJourney("myJourney")

// Пишем результат в память
memory.setAttr("isNewJourneyStarted", isNewJourneyStarted)
```

<p class="callout warning">При инициализации JourneyManager для лида будет создана персона, если она еще не была создана! В текущей версии плагин позволяет отслеживать пути персон и не возможно отслеживание путей лидов. Поэтому персона создается для каждого лида на самой ранней стадии пути, чтобы иметь возможность отслеживать путь как можно раньше - даже когда лид/персона еще не взаимодействовали с бизнесом.</p>

<p class="callout warning">В качестве роли персоны будет использована роль по умолчанию, которую необходимо задать в настройках чат-бота.</p>

</td></tr><tr style="height: 249.667px;"><td style="width: 9.76514%; height: 249.667px;">2</td><td style="width: 21.7789%; height: 249.667px;">Завершить путешествие

</td><td style="width: 68.456%; height: 249.667px;">```JavaScript
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Завершаем выбранно путешествие
let isJourneyCompleted = jm.completeJourney()

// Пишем результат в память
memory.setAttr("isJourneyCompleted", isJourneyCompleted)
```

</td></tr><tr style="height: 363.667px;"><td style="width: 9.76514%; height: 363.667px;">3</td><td style="width: 21.7789%; height: 363.667px;">Следующая фаза</td><td style="width: 68.456%; height: 363.667px;">```JavaScript
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Запоминаем название текущей фазы
let oldPhaseName = jm.getCurrentPhase().name

// Стартуем новыую фазу путешествия
let isPhaseStarted = jm.startNextPhase("nextPhaseCode")

// Запоминаем название новой фазы (если она установилась, конечно)
let newPhaseName = jm.getCurrentPhase().name

// Пишем результат в память
memory.setAttr("isPhaseStarted", isPhaseStarted)
memory.setAttr("oldPhaseName", oldPhaseName)
memory.setAttr("newPhaseName", newPhaseName)
```

</td></tr><tr style="height: 29.7917px;"><td style="width: 9.76514%; height: 29.7917px;">4</td><td style="width: 21.7789%; height: 29.7917px;">Предоставить пользу</td><td style="width: 68.456%; height: 29.7917px;">```JavaScript
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Сообщаем менеджеру путешествия, что мы предоставили пользу
let isValueGiven = jm.giveValue("valueCode")

// Загружаем информацию о метрике из справочника
let valueInfo = jm.getValueInfo("valueCode")

// Загружаем информацию о собранной метрике
let value = jm.getValue("valueCode")

// Выводим результат работы
memory.setAttr("isValueGiven", isValueGiven)
memory.setAttr("valueName", valueInfo.name)
memory.setAttr("valueWeight", value.weight)
```

</td></tr><tr style="height: 29.7917px;"><td style="width: 9.76514%; height: 29.7917px;">5</td><td style="width: 21.7789%; height: 29.7917px;">Достигнуть цель</td><td style="width: 68.456%; height: 29.7917px;">```Java
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Сообщаем менеджеру путешествия, что мы достигли цели
let isGoalAchieved = jm.achieveGoal("goalCode")

// Загружаем информацию о цели
let goal = jm.getGoalInfo(goalCode)

// Выводим результат работы
memory.setAttr("isGoalAchieved", isGoalAchieved)
memory.setAttr("goalName", goal.name)
```

</td></tr><tr><td style="width: 9.76514%;">6</td><td style="width: 21.7789%;">Записать показатель</td><td style="width: 68.456%;">```JavaScript
// Подключаем плагин
snippet("Common.DialogJourney.Manager")

// Создаем менеджер путешествия и передаем ему лид
let jm = new JourneyManager(lead)

// Выбираем путешествие
jm.selectJourney("myJourney")

// Считываем значение метрики
let metricValue = lead.getAttr("metricValue")

// Сохраняем метрику
let isMetricSaved = jm.saveJourneyMetric("metricCode", metricValue)

// Загружаем информацию о метрике из справочника
let metricInfo = jm.getMetricInfo("metricCode")

// Загружаем информацию о собранной метрике
let metric = jm.getLatestJourneyMetric("metricCode")

// Выводим результат работы
memory.setAttr("isMetricSaved", isMetricSaved)
memory.setAttr("metricName", metricInfo.name)
memory.setAttr("metricValue", metric.value)
```

</td></tr></tbody></table>

Другие примеры кода с использованием плагина смотрите в шаблоне чат-бота, доступного для скачивания <span style="text-decoration: underline;">здесь</span>.

### Интеграция со сторонними системами

Вы можете использовать DJ плагин для сбора данных о путешествии пользователей не только в чат-боте, созданном на платформе Metabot. Вы можете вести разработку на других бот-платформах и использовать Metabot для сбора и визуализации аналитики.

Также, вы можете отправлять в Metabot информацию о событиях из других систем, чтобы фиксировать прохождение пользователя по пути, когда пользователь совершает нужное целевое действие на сайте (например, размещает товар в корзину, но не оплачивает).

Для интеграции сторонних систем с вашим Dialog Journey в чат-боте на Metabot, необходимо настроить точки интеграции для всех необходимых событий, которые вам нужно регистрировать в Metabot. Информацию о точках интеграции смотрите в разделе [Точки интеграции и конструктор API](https://docs.metabot24.ru/books/metabot-platform/page/tocki-integracii-i-konstruktor-api "Точки интеграции и конструктор API"). В коде точки интеграции разработайте код согласно примерам выше.

# Business.Helpers.Response

Плагин Business.Helpers.Response служит утилитой для стандартизации ответов API в бизнес-приложениях. Его основная цель - упростить создание последовательных и структурированных ответов для различных сценариев, возникающих во время взаимодействия с API.

```JavaScript
/** 
 * Prepares and returns an API response object for a failed operation.
 * This should be used when an operation does not complete successfully,
 * with the provided error message included in the response.
 * 
 * @param {string} errorMessage - The error message to be included in the response.
 * @returns {Object} An object representing a failed operation response.
 */
function getErrorResponse(errorMessage) {
  return {
    success: false,
    message: errorMessage
  };  
}

/** 
 * Prepares and returns an API response object for a successful operation.
 * This should be used when an operation completes successfully 
 * and no additional data needs to be returned.
 * 
 * @returns {Object} An object representing a successful operation response.
 */
function getSuccessResponse() {
  return {
    success: true  
  };
}

/** 
 * Prepares and returns an API response object for a successful operation 
 * with additional JSON data. This should be used when an operation completes 
 * successfully and there is additional data to return in the response.
 * 
 * @param {Object} json - The JSON data to be included in the response.
 * @returns {Object} An object representing a successful operation response with additional data.
 */
function getSuccessResponseWithJson(json) {
  return {
      ...json,
      success: true
  }
}
```

# Интеграция с Google Sheets

Первым делом необходимо создать новую таблицу в Google Sheets и добавить нового редактора <span class="notion-enable-hover" data-token-index="1" spellcheck="false"><api@metabot.org>.</span>

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2024-02/scaled-1680-/o7zUHZfmlqXpHIuj-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2024-02/o7zUHZfmlqXpHIuj-image.png)

<span class="notion-enable-hover" data-token-index="1" spellcheck="false">Далее копируем ID таблицы из адресной строки.</span>

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2024-02/scaled-1680-/w1LvONxKtdNhOBuC-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2024-02/w1LvONxKtdNhOBuC-image.png)

<span class="notion-enable-hover" data-token-index="1" spellcheck="false">Копируем название листа.</span>

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2024-02/scaled-1680-/XH2zcN4vXSH6TU4E-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2024-02/XH2zcN4vXSH6TU4E-image.png)

<span class="notion-enable-hover" data-token-index="1" spellcheck="false">Копируем название столбцов в таблице.</span>

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2024-02/scaled-1680-/kQKAKqYgTt3NZqSc-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2024-02/kQKAKqYgTt3NZqSc-image.png)

### Метод для добавления нового столбца

Записываем в нужное место скрипта следующий код:

```JavaScript
var GoogleSheetsService = require('Common.Integrations.GoogleSheets') // Плагин для работы с Google Sheets

GoogleSheetsService.sheetId = '11muAnepqhpRQ9ElE9CzC3E-edmf9JbRE3gwmBTDa5pE' // ID скопированный из таблицы
GoogleSheetsService.listName = 'list' // Название листа

// Параметры где ключ - название столбца, значение - данные которые занесутся в строку
let params = {
	"region": "Москвская область",
	"name": 'Тест',
	"age": "24",
	"city": "Москва",
}

let result = GoogleSheetsService.addRow(params) // Функция для добавления строк в таблицу

debug(result) // Вернётся результат выполенния с Id в строки в которую записались данные
```

<span class="notion-enable-hover" data-token-index="1" spellcheck="false"></span>

<span class="notion-enable-hover" data-token-index="1" spellcheck="false">Пример ответа:</span>

```JavaScript
{
    "status": "success",
    "message": "Row added successfully", // Сообщение, если есть ошибка - вернётся описание ошибки
    "rowId": 8 // Id в строки в которую записались данные
}
```

### <span class="notion-enable-hover" data-token-index="1" spellcheck="false">Метод для поиска и замены значения в ячейке</span>

<span class="notion-enable-hover" data-token-index="1" spellcheck="false">Записываем в нужное место скрипта следующий код:</span>

```JavaScript
var GoogleSheetsService = require('Common.Integrations.GoogleSheets') // Плагин для работы с Google Sheets

GoogleSheetsService.sheetId = '11muAnepqhpRQ9ElE9CzC3E-edmf9JbRE3gwmBTDa5pE' // ID скопированный из таблицы
GoogleSheetsService.listName = 'list' // Название листа

// Параметры со настройками для замены
let params = {
    colomn_search_name: 'region',
    colomn_edit_name: 'region',
    search_value: '123123',
    match_entire_cell: true,
    new_value: "Антон"
}

let result = GoogleSheetsService.searchAndEditRow(params) // Функция для поиска и замены строк

debug(result) // Вернётся результат выполенния или код ошибки
```

<span class="notion-enable-hover" data-token-index="1" spellcheck="false">Пример ответа:</span>

```JavaScript
{
    "status": "success",
    "message": 'Значение найдёно и измененно'
}
```

# Документация Telegram-плагина

<table border="1" id="bkmrk-%D0%90%D0%B2%D1%82%D0%BE%D1%80-%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%B0-%D0%9F%D0%B5%D1%82%D1%80%D0%BE%D0%B2" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; width: 102.469%; height: 154.935px;"><tbody><tr style="height: 29.7917px;"><td style="width: 38.2087%; height: 29.7917px;">**Название плагина**</td><td style="width: 61.7894%; height: 29.7917px;">Telegram</td></tr><tr style="height: 29.7917px;"><td style="width: 38.2087%; height: 29.7917px;">**Разработчик**</td><td style="width: 61.7894%; height: 29.7917px;">Официальные плагины от Metabot</td></tr><tr style="height: 35.7682px;"><td class="align-left" style="width: 38.2087%; height: 35.7682px;">**Авторы**</td><td style="width: 61.7894%; height: 35.7682px;">Борисов Павел (https://t.me/mr\_result)

</td></tr><tr style="height: 29.7917px;"><td class="align-left" style="width: 38.2087%; height: 29.7917px;">**Дата создания**</td><td style="width: 61.7894%; height: 29.7917px;">02 Октября 2023</td></tr><tr style="height: 29.7917px;"><td class="align-left" style="width: 38.2087%; height: 29.7917px;">**Последняя дата обновления**</td><td style="width: 61.7894%; height: 29.7917px;">25 Апреля 2024</td></tr></tbody></table>

### Описание

Этот плагин предоставляет удобный интерфейс для работы с Telegram Bot API. Он поддерживает отправку текстовых сообщений, фотографий, создание клавиатур и обработку ответов пользователя.

### Основной класс TelegramMessage

#### Подключение и инициализация

```JavaScript
let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()
```

#### Основные параметры

```JavaScript
msg.text = "Ваше сообщение" // Текст сообщения
msg.parse_mode = "HTML" // Режим форматирования (HTML или MarkdownV2)
msg.protect_content = true // Защита контента от пересылки
msg.keyboard = "Да[yes]==Нет[no]" // Создание клавиатуры
```

#### Форматы клавиатуры

- <span class="notion-enable-hover" data-token-index="0">**Вертикальное разделение** —</span> используйте **<span class="notion-enable-hover" data-token-index="2" spellcheck="false">==</span>**<span class="notion-enable-hover" data-token-index="2" spellcheck="false">;</span>

```JavaScript
msg.keyboard = "Кнопка1[btn1]==Кнопка2[btn2]" // Кнопки будут расположены вертикально
```

- <span class="notion-enable-hover" data-token-index="0">**Горизонтальное разделение** —</span> используйте **<span class="notion-enable-hover" data-token-index="0" spellcheck="false">||</span>**<span class="notion-enable-hover" data-token-index="2" spellcheck="false">;</span>

```JavaScript
msg.keyboard = "Кнопка1[btn1]||Кнопка2[btn2]" // Кнопки будут расположены горизонтально
```

- <span class="notion-enable-hover" data-token-index="2" spellcheck="false">**Комбинированное разделение**;</span>

```JavaScript
msg.keyboard = "Кнопка1[btn1]||Кнопка2[btn2]==Кнопка3[btn3]||Кнопка4[btn4]"
```

#### Специальные типы кнопок

- **Запрос контакта**:

```JavaScript
msg.keyboard = "Отправить контакт[telegram_contact]"
```

- **Запрос локации**:

```JavaScript
msg.keyboard = "Отправить локацию[telegram_location]"
```

- **Ссылки**:

```JavaScript
msg.keyboard = "Посетить сайт[<https://example.com>]"
```

- **Web App**:

```JavaScript
msg.keyboard = "Открыть приложение{<https://webapp-url.com>}"
```

- **Ссылка на пользователя**:

```JavaScript
msg.keyboard = "Написать админу[tg://user?id=123456789]"
```

### Примеры использования

#### Простое меню с обработкой ответов

Код будет работать корректно только в команде [<span class="link-annotation-unknown-block-id--1055303680">JS Callback.</span>](https://docs.metabot24.ru/books/03-nastroika-botov/page/vypolnit-javascript-callback)

```JavaScript
let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()
msg.text = "Выберите действие:"
msg.keyboard = "Информация[info]==Помощь[help]==Настройки[settings]"
msg.parse_mode = "HTML"

switch (true) {
    case (isFirstImmediateCall):
        msg.send()
        return false

    case (msg.getMessagePayload()?.callback_data == "info"):
        msg.addReplyToText()
        bot.sendMessage("Информация о боте...")
        return false

    case (msg.getMessagePayload()?.callback_data == "help"):
        msg.addReplyToText()
        bot.sendMessage("Справка по использованию...")
        return false

    case (msg.getMessagePayload()?.callback_data == "settings"):
        msg.addReplyToText()
        bot.runScriptForLead(123, leadId) // Запуск скрипта настроек
        return false
}
```

### Справочник методов

#### Основные методы отправки

- **send()** <span class="notion-enable-hover" data-token-index="0">— </span>отправка нового сообщения;
- **edit()** <span class="notion-enable-hover" data-token-index="0">— </span>редактирование существующего сообщения;
- **addReplyToText()** <span class="notion-enable-hover" data-token-index="0">— </span>добавление ответа к существующему сообщению;
- **removeInlineKeyboard()** <span class="notion-enable-hover" data-token-index="0">— </span>удаление клавиатуры.

#### Получение информации

- **getMessagePayload()** — получение информации о сообщении из вебхука:  
    
    - **message\_id** — ID сообщения;
    - **text** — текст сообщения/кнопки;
    - **input\_type** — тип ввода ('write'/'press');
    - **callback\_data** — данные колбэка;
    - **payload** — полные данные вебхука.

<span style="font-size: 1.666em; font-weight: 400;">Обработка ответов пользователя</span>

```JavaScript
// Проверка типа ввода
if (msg.getMessagePayload()?.input_type == "write") {
    // Пользователь написал текст
}

// Проверка нажатия кнопки
if (msg.getMessagePayload()?.callback_data == "button_id") {
    // Пользователь нажал кнопку
}
```

#### Отладка

```JavaScript
msg.debug("Отладочное сообщение") // Отправка отладочной информации. 
								  //Не отправляется пользователю но логируется в карточке лида
```

### Лучшие практики

- <span class="notion-enable-hover" data-token-index="0">Всегда проверяйте первый вызов</span>:

```JavaScript
if (isFirstImmediateCall) {
    msg.send()
    return false
}
```

- <span class="notion-enable-hover" data-token-index="0">Добавляйте обработку текстовых сообщений</span>:

```JavaScript
if (msg.getMessagePayload()?.input_type == "write") {
    bot.sendMessage('Пожалуйста, используйте кнопки')
    return false
}
```

- <span class="notion-enable-hover" data-token-index="0">Скрывайте клавиатуру после использования</span>:

```JavaScript
msg.addReplyToText() // После нажатия на кнопку записывает её в сообщение
```

#### Форматирование текста и entities

В плагине доступно два способа форматирования текста: **HTML** и **MarkdownV2**. По умолчанию параметр **<span class="notion-enable-hover" data-token-index="1" spellcheck="false">parse\_mode</span>** не установлен — это сделано специально для возможности работы с **entities** (когда нужно сохранить форматирование текста, которое пользователь отправил в Telegram).

#### HTML форматирование

<p class="callout info align-left">Документация: [<span class="link-annotation-unknown-block-id-1917182307">https://core.telegram.org/bots/api#html-style</span>](https://core.telegram.org/bots/api#html-style)</p>

```JavaScript
msg.text = "<b>Жирный текст</b>\n<i>Курсив</i>\nМоноширинный текст"
msg.parse_mode = "HTML"
```

#### MarkdownV2 форматирование

<p class="callout info align-left">Документация: [<span class="link-annotation-unknown-block-id--1697982877">https://core.telegram.org/bots/api#markdownv2-style</span>](https://core.telegram.org/bots/api#markdownv2-style)</p>

```JavaScript
msg.text = "**Жирный текст**\n__Курсив__\n`Моноширинный текст`"
msg.parse_mode = "MarkdownV2"
```

#### Работа с entities

Entities используются, когда нужно сохранить форматирование текста, которое пользователь отправил в Telegram. Это особенно полезно при создании различных конструкторов сообщений или при пересылке сообщений с сохранением форматирования.

Пример сохранения форматированного сообщения пользователя:

```JavaScript
let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()

msg.text = `Введите текст для рассылки`
msg.keyboard = `↩️ Назад[${backScripts}]`
msg.parse_mode = 'HTML'

switch (true) {
    case (isFirstImmediateCall):
        msg.send()
        return false

    case (msg.getMessagePayload()?.input_type == "write"):
        msg.removeInlineKeyboard()
        // Получаем webhook и извлекаем текст с entities
        let webhook = msg.getMessagePayload()
        let messageData = {
            "text": webhook?.payload?.message?.text,
            "entities": webhook?.payload?.message?.entities // Сохраняем entities для сохранения форматирования
        }

        // Сохраняем сообщение с форматированием в базу данных
        lead.setJsonAttr("messageData", messageData)

        bot.run({
            script_id: nextScript
        })
        return false

    default:
        msg.addReplyToText()
        bot.run({
            script_id: msg.getMessagePayload().callback_data
        })
        return true
}
```

В этом примере:

1. Когда пользователь отправляет форматированное сообщение, мы получаем не только сам текст, но и массив entities.
2. Entities содержат информацию о форматировании: жирный текст, курсив, ссылки и т.д.
3. Мы сохраняем и текст, и entities, чтобы позже можно было воспроизвести сообщение с точно таким же форматированием.
4. **Важно**: для работы с entities параметр **parse\_mode** должен быть не установлен (режим по умолчанию).

Теперь после запоминания **entities** мы можем сделать его вывод таким образом:

```JavaScript
let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()

let messageData = lead.getJsonAttr("messageData")

msg.text = messageData?.text
msg.entities = messageData?.entities

// Если попробовать использовать parse_mode то форматирование будет некорректным
```

### Работа с файлами

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

### Получение и обработка файлов

```JavaScript
// Подключаем библиотеку
let TelegramMessage = require('Common.Integrations.Telegram')
let msg = new TelegramMessage()

// Все входящие файлы
let attachments = bot.getAllAttachments()

if(Boolean(attachments?.[0]?.url)){

uploadData = bot.downloadFileFromUrl(attachments[0].url)

msg.debug('Пользователь отправил файл: ' + uploadData.url)
msg.debug('Файл ID : ' + JSON.stringify(attachments[0]?.payload?.file_id)) // данная строка вспомогательная для
																		   // автоматизации вывода полученных 
lead.setAttr('file', uploadData.url)									   // файлов в других местах бота
// Запуск скрипта...
bot.runScriptByCodeForLead("recieveFile", lead.getData('id'))

bot.stop()
}
```

В этом примере:

1. Проверяем наличие прикрепленных файлов с помощью **bot.getAllAttachments()**.
2. Если файл есть, скачиваем его через **bot.downloadFileFromUrl()**.
3. Логируем получение файла с помощью **msg.debug()**.
4. Сохраняем URL файла в атрибут лида.
5. Запускаем скрипт обработки файла.
6. Останавливаем текущий скрипт.

Дополнительный пример:

```JavaScript
let TelegramMessage = require('Common.Integrations.Telegram')

let msg = new TelegramMessage()

msg.text = 'А теперь жду твоё фото 😉'
msg.keyboard = ``
msg.parse_mode = 'HTML'

let scriptCode = bot.getCurrentScriptCode()
lead.setAttr("script_code", scriptCode)

let data = bot.getAllAttachments()

switch (true) {

    case (isFirstImmediateCall):

        msg.send()
        return false
        break

    case (Boolean(data?.[0]?.type != 'image')):

        bot.sendMessage('Отправь сжатое изображение. Такой формат не подходит')
        return false
        break

    case (Boolean(data?.[0]?.type == 'image')):

        uploadData = bot.downloadFileFromUrl(data[0].url)
        msg.debug('Пользователь отправил файл: ' + uploadData.url)
        lead.setAttr('photo', uploadData.url)
        bot.sendMessage('Класс! Фото загрузил')
        return true
        break

    default:

        msg.addReplyToText()
        return true
}
```

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

- Всегда проверяйте наличие файлов перед их обработкой;
- Логируйте получение файлов для отладки;
- Сохраняйте URL файлов в атрибуты лида для дальнейшего использования;
- Запускайте отдельный скрипт для обработки файлов, чтобы разделить логику.

# Плагин для работы с CustomStorage

# Плагин CustomStorage

Плагин **CustomStorage** предназначен для работы с кастомными таблицами атрибутов в JavaScript скриптах. Плагин позволяет сохранять и получать данные из кастомных таблиц, аналогично работе с атрибутами лидов через `lead.setAttr()` и `lead.getAttr()` и т.д.

Плагин поддерживает:
- Работу с обычными атрибутами (строки, числа, булевы значения)
- Работу с JSON атрибутами (объекты, массивы, вложенные структуры)
- Точечную нотацию для вложенных JSON структур (например, `user.profile.name`)
- Подкатегории для группировки данных через `key2` и `key3`
- Прямую работу с базой данных без кэширования в памяти

---

## Первоначальная настройка (Setup)

Перед использованием плагина необходимо создать кастомную таблицу для хранения атрибутов. Это можно сделать двумя способами:

### Способ 1: Автоматическое создание через метод `setup()`

Метод `setup()` автоматически создает кастомную таблицу со всеми необходимыми полями и уникальным индексом.

**Важно:** Метод `setup()` можно вызвать только один раз для каждой таблицы. При повторном вызове будет выброшена ошибка, если таблица уже существует.

**Пример использования:**

```javascript
let customStorage = require('Common.Platform.CustomStorage')

// Создаем новую таблицу для атрибутов
customStorage.setup('my_attributes_table')

// Теперь можно работать с таблицей
customStorage.setAttr('server_name', 'production-server-01')
```

**Что делает метод `setup()`:**
1. Проверяет, что таблицы с таким именем еще не существует
2. Создает кастомную таблицу с необходимыми полями:
   - `id` - автоинкрементный идентификатор
   - `key` - первый ключ (обязательный)
   - `key2` - второй ключ (опциональный, для подкатегорий)
   - `key3` - третий ключ (опциональный, для подкатегорий)
   - `value_type` - тип значения (`string` или `json`)
   - `value` - значение атрибута
   - `created_at` - дата создания
   - `updated_at` - дата обновления
3. Создает уникальный индекс на комбинацию `(key, key2, key3)` для предотвращения дубликатов

### Способ 2: Ручное создание через интерфейс админки

Вы можете создать таблицу вручную через интерфейс админки в разделе **Таблицы**, используя структуру из JSON примера ниже.

**Структура таблицы (JSON для импорта):**

```json
{
    "version": "2.50.0",
    "format": 2,
    "custom_tables": {
        "business_attributes": {
            "is_enabled": 1,
            "name": "business_attributes",
            "title": "Атрибуты бизнеса",
            "comment": null,
            "is_sync_struct": 1,
            "is_show_in_menu": 1,
            "has_api_access": 0,
            "sort_order": 0,
            "is_compact_view": 0,
            "line_height": 1,
            "max_width_px": null,
            "css": null,
            "js": null,
            "sort_order_data": 1,
            "fields": {
                "id": {
                    "is_enabled": 1,
                    "name": "id",
                    "title": "ID",
                    "type": "AUTOINC",
                    "is_required": 1,
                    "size": null,
                    "default_value": null,
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 0,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                },
                "key": {
                    "is_enabled": 1,
                    "name": "key",
                    "title": "Первый ключ",
                    "type": "TEXT",
                    "is_required": 1,
                    "size": null,
                    "default_value": "",
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 2,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                },
                "key2": {
                    "is_enabled": 1,
                    "name": "key2",
                    "title": "Второй ключ",
                    "type": "TEXT",
                    "is_required": 0,
                    "size": null,
                    "default_value": "",
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 3,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                },
                "key3": {
                    "is_enabled": 1,
                    "name": "key3",
                    "title": "Третий ключ",
                    "type": "TEXT",
                    "is_required": 0,
                    "size": null,
                    "default_value": "",
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 4,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                },
                "value_type": {
                    "is_enabled": 1,
                    "name": "value_type",
                    "title": "Тип значения",
                    "type": "TEXT",
                    "is_required": 0,
                    "size": null,
                    "default_value": "",
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 5,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                },
                "value": {
                    "is_enabled": 1,
                    "name": "value",
                    "title": "Значение",
                    "type": "TEXT",
                    "is_required": 1,
                    "size": null,
                    "default_value": "",
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 6,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                },
                "created_at": {
                    "is_enabled": 1,
                    "name": "created_at",
                    "title": "Дата создания",
                    "type": "CREATED_AT",
                    "is_required": 1,
                    "size": null,
                    "default_value": "NOW",
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 7,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                },
                "updated_at": {
                    "is_enabled": 1,
                    "name": "updated_at",
                    "title": "Дата изменения",
                    "type": "UPDATED_AT",
                    "is_required": 0,
                    "size": null,
                    "default_value": null,
                    "hint": null,
                    "tooltip": null,
                    "sort_order": 8,
                    "precision": null,
                    "is_sync_struct": 1,
                    "is_locked_for_regular_user": 0,
                    "max_width_px": null
                }
            }
        }
    }
}
```

**Важно:** После создания таблицы вручную необходимо создать уникальный индекс на комбинацию `(key, key2, key3)` в базе данных.

---

## Подключение плагина

Для использования плагина в JavaScript скрипте необходимо подключить его через `require()`:

```javascript
let customStorage = require('Common.Platform.CustomStorage')
```

После подключения необходимо установить имя таблицы, с которой будет работать плагин:

```javascript
customStorage.setTableName('business_attributes')
```

---

## Методы работы с таблицей

### setTableName()

Устанавливает имя кастомной таблицы для работы.

**Сигнатура:**
```javascript
customStorage.setTableName(string $tableName): self
```

**Параметры:**
- `$tableName` (string) - Имя кастомной таблицы, которая должна существовать в текущем бизнесе

**Возвращает:** `self` - Возвращает сам объект для цепочки вызовов

**Пример:**
```javascript
customStorage.setTableName('business_attributes')
```

---

### getTableName()

Возвращает имя текущей установленной таблицы.

**Сигнатура:**
```javascript
customStorage.getTableName(): string|null
```

**Возвращает:** `string|null` - Имя таблицы или `null`, если таблица не установлена

**Пример:**
```javascript
let tableName = customStorage.getTableName()
bot.sendText('Работаем с таблицей: ' + tableName)
```

---

### setup()

Создает новую кастомную таблицу для атрибутов со всеми необходимыми полями и уникальным индексом.

**Сигнатура:**
```javascript
customStorage.setup(string $tableName): self
```

**Параметры:**
- `$tableName` (string) - Имя создаваемой таблицы

**Возвращает:** `self` - Возвращает сам объект для цепочки вызовов

**Исключения:**
- Выбрасывает ошибку, если таблица с таким именем уже существует
- Выбрасывает ошибку, если не указан Business ID

**Пример:**
```javascript
// Создаем новую таблицу
customStorage.setup('my_attributes_table')

// Теперь можно работать с ней
customStorage.setAttr('test_key', 'test_value')
```

**Важно:** Метод `setup()` автоматически устанавливает созданную таблицу как текущую, поэтому после вызова `setup()` можно сразу начинать работу с атрибутами.

---

## Методы работы с обычными атрибутами

### setAttr()

Устанавливает значение обычного атрибута в базе данных.

**Сигнатура:**
```javascript
customStorage.setAttr(string $key, mixed $value, ?string $key2 = null, ?string $key3 = null): self
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$value` (mixed) - Значение атрибута (строка, число, булево значение)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `self` - Возвращает сам объект для цепочки вызовов

**Примеры:**
```javascript
// Простое сохранение
customStorage.setAttr('server_name', 'production-server-01')
customStorage.setAttr('server_port', '8080')

// С подкатегориями
customStorage.setAttr('server_name', 'server-01', 'region1', 'dc1')
customStorage.setAttr('server_ip', '192.168.1.1', 'region1', 'dc1')
```

**Примечание:** Если для комбинации `(key, key2, key3)` уже существует запись, она будет обновлена. Если найдено несколько записей, будет выброшена ошибка.

---

### getAttr()

Получает значение обычного атрибута из базы данных.

**Сигнатура:**
```javascript
customStorage.getAttr(string $key, ?string $key2 = null, ?string $key3 = null): mixed|null
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `mixed|null` - Значение атрибута или `null`, если атрибут не найден

**Примеры:**
```javascript
// Простое получение
let serverName = customStorage.getAttr('server_name')

// С подкатегориями
let serverName = customStorage.getAttr('server_name', 'region1', 'dc1')
let serverIp = customStorage.getAttr('server_ip', 'region1', 'dc1')
```

**Примечание:** Если для комбинации `(key, key2, key3)` найдено несколько записей, будет выброшена ошибка.

---

### getIntAttr()

Получает значение атрибута, преобразованное в целое число.

**Сигнатура:**
```javascript
customStorage.getIntAttr(string $key, ?int $default = 0, ?string $key2 = null, ?string $key3 = null): int|null
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$default` (int|null) - Значение по умолчанию, если атрибут не найден (по умолчанию: `0`)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `int|null` - Целое число или значение по умолчанию

**Примеры:**
```javascript
// Получение с значением по умолчанию
let port = customStorage.getIntAttr('server_port', 8080)
let nonExistent = customStorage.getIntAttr('non_existent', 100) // вернет 100

// С подкатегориями
let port = customStorage.getIntAttr('server_port', 8080, 'region1', 'dc1')
```

---

### getFloatAttr()

Получает значение атрибута, преобразованное в число с плавающей точкой.

**Сигнатура:**
```javascript
customStorage.getFloatAttr(string $key, ?float $default = 0.0, ?string $key2 = null, ?string $key3 = null): float|null
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$default` (float|null) - Значение по умолчанию, если атрибут не найден (по умолчанию: `0.0`)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `float|null` - Число с плавающей точкой или значение по умолчанию

**Примеры:**
```javascript
let price = customStorage.getFloatAttr('price', 0.0)
let nonExistent = customStorage.getFloatAttr('non_existent', 2.5) // вернет 2.5
```

---

### getBoolAttr()

Получает значение атрибута, преобразованное в булево значение.

**Сигнатура:**
```javascript
customStorage.getBoolAttr(string $key, ?bool $default = false, ?string $key2 = null, ?string $key3 = null): bool|null
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$default` (bool|null) - Значение по умолчанию, если атрибут не найден (по умолчанию: `false`)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `bool|null` - Булево значение или значение по умолчанию

**Особенности обработки строковых значений:**
- Строки `'false'`, `'0'`, `''`, `'no'`, `'off'` преобразуются в `false`
- Строки `'true'`, `'1'`, `'yes'`, `'on'` преобразуются в `true`
- Остальные строки преобразуются по стандартным правилам PHP

**Примеры:**
```javascript
let isActive = customStorage.getBoolAttr('is_active', false)
let nonExistent = customStorage.getBoolAttr('non_existent', true) // вернет true

// Строковые значения обрабатываются корректно
customStorage.setAttr('bool_true', 'true')
customStorage.setAttr('bool_false', 'false')
let val1 = customStorage.getBoolAttr('bool_true') // вернет true
let val2 = customStorage.getBoolAttr('bool_false') // вернет false
```

---

### isAttrExist()

Проверяет существование атрибута в базе данных.

**Сигнатура:**
```javascript
customStorage.isAttrExist(string $key, ?string $key2 = null, ?string $key3 = null): bool
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `bool` - `true`, если атрибут существует, `false` - если не существует

**Примеры:**
```javascript
if (customStorage.isAttrExist('server_name')) {
    bot.sendText('Сервер найден: ' + customStorage.getAttr('server_name'))
} else {
    bot.sendText('Сервер не найден')
}

// С подкатегориями
if (customStorage.isAttrExist('server_name', 'region1', 'dc1')) {
    bot.sendText('Сервер найден в регионе 1')
}
```

---

### issetAttr()

Проверяет существование атрибута в базе данных (аналог `isAttrExist()`).

**Сигнатура:**
```javascript
customStorage.issetAttr(string $key, ?string $key2 = null, ?string $key3 = null): bool
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `bool` - `true`, если атрибут существует, `false` - если не существует

**Примеры:**
```javascript
if (customStorage.issetAttr('server_name')) {
    bot.sendText('Сервер найден')
}
```

---

### deleteAttr()

Удаляет атрибут из базы данных.

**Сигнатура:**
```javascript
customStorage.deleteAttr(string $key, ?string $key2 = null, ?string $key3 = null): self
```

**Параметры:**
- `$key` (string) - Ключ атрибута (обязательный)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `self` - Возвращает сам объект для цепочки вызовов

**Примеры:**
```javascript
// Удаление простого атрибута
customStorage.deleteAttr('server_port')

// Удаление с подкатегориями
customStorage.deleteAttr('server_name', 'region1', 'dc1')
```

---

### getAllAttr()

Получает все обычные атрибуты (не JSON) из базы данных.

**Сигнатура:**
```javascript
customStorage.getAllAttr(): array
```

**Возвращает:** `array` - Ассоциативный массив всех атрибутов, где ключ - это комбинация `key`, `key2`, `key3` (разделенные точками), а значение - значение атрибута

**Примеры:**
```javascript
let allAttrs = customStorage.getAllAttr()
bot.sendText('Всего атрибутов: ' + Object.keys(allAttrs).length)

// Пример структуры результата:
// {
//     "server_name": "server-01",
//     "server_name.region1.dc1": "server-01",
//     "server_ip.region1.dc1": "192.168.1.1"
// }
```

---

## Методы работы с JSON атрибутами

### setJsonAttr()

Устанавливает значение JSON атрибута в базе данных. Поддерживает точечную нотацию для вложенных структур.

**Сигнатура:**
```javascript
customStorage.setJsonAttr(string $key, mixed $value, ?string $key2 = null, ?string $key3 = null): self
```

**Параметры:**
- `$key` (string) - Ключ атрибута. Может использоваться точечная нотация для вложенных структур (например, `user.profile.name`)
- `$value` (mixed) - Значение для установки. Может быть объектом, массивом, строкой, числом, булевым значением
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный, не связан с вложенностью JSON)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный, не связан с вложенностью JSON)

**Возвращает:** `self` - Возвращает сам объект для цепочки вызовов

**Особенности:**
- **Точечная нотация:** При использовании точечной нотации (например, `user.profile.name`) значение устанавливается внутрь существующего JSON объекта. Верхний ключ (в данном случае `user`) используется как `key` в БД, а остальные части пути создают вложенную структуру.
- **Без точечной нотации:** Если точечная нотация не используется, значение должно быть массивом или объектом (для верхнего уровня).
- **С точечной нотацией:** Разрешены любые типы значений (строки, числа, bool, массивы, объекты).

**Примеры:**
```javascript
// Простой JSON объект
customStorage.setJsonAttr('config', {
    timeout: 30,
    retries: 3,
    enabled: true
})

// Точечная нотация для вложенных значений
customStorage.setJsonAttr('user.profile.name', 'John Doe')
customStorage.setJsonAttr('user.profile.email', 'john@example.com')
customStorage.setJsonAttr('user.profile.age', 30)
customStorage.setJsonAttr('user.settings.theme', 'dark')

// Глубокая вложенность
customStorage.setJsonAttr('app.settings.database.host', 'localhost')
customStorage.setJsonAttr('app.settings.database.port', 5432)
customStorage.setJsonAttr('app.settings.cache.enabled', true)

// С подкатегориями (key2, key3)
customStorage.setJsonAttr('server.config', { cpu: 8, ram: 32 }, 'region1', 'dc1')
customStorage.setJsonAttr('server.config', { cpu: 16, ram: 64 }, 'region2', 'dc2')

// Массивы
customStorage.setJsonAttr('packages', [
    { name: 'nginx', version: '1.18.0' },
    { name: 'php', version: '8.1.0' },
    { name: 'mysql', version: '8.0.0' }
])
```

**Примечание:** Если для комбинации `(key, key2, key3)` уже существует запись, JSON объект будет обновлен. Если найдено несколько записей, будет выброшена ошибка.

---

### getJsonAttr()

Получает значение JSON атрибута из базы данных. Поддерживает точечную нотацию для извлечения вложенных значений.

**Сигнатура:**
```javascript
customStorage.getJsonAttr(string $key, ?string $key2 = null, ?string $key3 = null): mixed|null
```

**Параметры:**
- `$key` (string) - Ключ атрибута. Может использоваться точечная нотация для вложенных структур (например, `user.profile.name`)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `mixed|null` - Значение JSON атрибута или `null`, если атрибут не найден

**Примеры:**
```javascript
// Получение простого JSON объекта
let config = customStorage.getJsonAttr('config')
if (config) {
    bot.sendText('Timeout: ' + config.timeout)
    bot.sendText('Retries: ' + config.retries)
}

// Получение вложенных значений через точечную нотацию
let userName = customStorage.getJsonAttr('user.profile.name')
let userEmail = customStorage.getJsonAttr('user.profile.email')
let userAge = customStorage.getJsonAttr('user.profile.age')

// Получение всего объекта верхнего уровня
let userProfile = customStorage.getJsonAttr('user')
// userProfile будет содержать: { profile: { name: "John Doe", email: "john@example.com", age: 30 }, settings: { theme: "dark" } }

// Глубокая вложенность
let dbHost = customStorage.getJsonAttr('app.settings.database.host')
let dbPort = customStorage.getJsonAttr('app.settings.database.port')

// С подкатегориями
let serverConfig = customStorage.getJsonAttr('server.config', 'region1', 'dc1')
if (serverConfig) {
    bot.sendText('CPU: ' + serverConfig.cpu + ' cores')
    bot.sendText('RAM: ' + serverConfig.ram + ' GB')
}
```

**Примечание:** Если для комбинации `(key, key2, key3)` найдено несколько записей, будет выброшена ошибка.

---

### isJsonAttrKeyExist()

Проверяет существование JSON атрибута в базе данных.

**Сигнатура:**
```javascript
customStorage.isJsonAttrKeyExist(string $key, ?string $key2 = null, ?string $key3 = null): bool
```

**Параметры:**
- `$key` (string) - Ключ атрибута. Может использоваться точечная нотация (например, `user.profile.name`)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `bool` - `true`, если JSON атрибут существует, `false` - если не существует

**Примеры:**
```javascript
if (customStorage.isJsonAttrKeyExist('user.profile.name')) {
    bot.sendText('Имя пользователя: ' + customStorage.getJsonAttr('user.profile.name'))
}

// С подкатегориями
if (customStorage.isJsonAttrKeyExist('server.config', 'region1', 'dc1')) {
    bot.sendText('Конфигурация сервера найдена')
}
```

---

### issetJsonAttr()

Проверяет существование JSON атрибута в базе данных (аналог `isJsonAttrKeyExist()`).

**Сигнатура:**
```javascript
customStorage.issetJsonAttr(string $key, ?string $key2 = null, ?string $key3 = null): bool
```

**Параметры:**
- `$key` (string) - Ключ атрибута. Может использоваться точечная нотация (например, `user.profile.name`)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `bool` - `true`, если JSON атрибут существует, `false` - если не существует

**Примеры:**
```javascript
if (customStorage.issetJsonAttr('user.profile.email')) {
    bot.sendText('Email установлен')
}
```

---

### deleteJsonAttr()

Удаляет JSON атрибут из базы данных.

**Сигнатура:**
```javascript
customStorage.deleteJsonAttr(string $key, ?string $key2 = null, ?string $key3 = null): self
```

**Параметры:**
- `$key` (string) - Ключ атрибута. Может использоваться точечная нотация (например, `user.profile.name`)
- `$key2` (string|null) - Второй ключ для подкатегории (опциональный)
- `$key3` (string|null) - Третий ключ для подкатегории (опциональный)

**Возвращает:** `self` - Возвращает сам объект для цепочки вызовов

**Важно:** При удалении через точечную нотацию удаляется весь объект верхнего уровня. Например, `deleteJsonAttr('user')` удалит всю запись с `key='user'`, включая все вложенные значения (`user.profile.name`, `user.profile.email` и т.д.).

**Примеры:**
```javascript
// Удаление простого JSON атрибута
customStorage.deleteJsonAttr('config')

// Удаление вложенного JSON атрибута (удаляет весь верхний уровень)
customStorage.deleteJsonAttr('user') // Удалит всю запись с key='user'

// Удаление с подкатегориями
customStorage.deleteJsonAttr('server.config', 'region1', 'dc1')
```

---

### getAllJsonAttrs()

Получает все JSON атрибуты из базы данных.

**Сигнатура:**
```javascript
customStorage.getAllJsonAttrs(): array
```

**Возвращает:** `array` - Ассоциативный массив всех JSON атрибутов, где ключ - это комбинация `key`, `key2`, `key3` (разделенные точками), а значение - распарсенный JSON объект

**Примеры:**
```javascript
let allJsonAttrs = customStorage.getAllJsonAttrs()
bot.sendText('Всего JSON атрибутов: ' + Object.keys(allJsonAttrs).length)

// Пример структуры результата:
// {
//     "config": { timeout: 30, retries: 3, enabled: true },
//     "user": { profile: { name: "John Doe", email: "john@example.com" } },
//     "server.region1.dc1": { config: { cpu: 8, ram: 32 } }
// }
```

---

## Полный справочник методов

### Методы работы с таблицей

<table border="1" style="border-collapse: collapse; width: 100%;">
<tbody>
<tr style="background-color: #c2e0f4;">
<td style="width: 50%;">Метод</td>
<td style="width: 50%;">Описание</td>
</tr>
<tr>
<td><strong>setTableName(string $tableName): self</strong></td>
<td>Устанавливает имя кастомной таблицы для работы</td>
</tr>
<tr>
<td><strong>getTableName(): string|null</strong></td>
<td>Возвращает имя текущей установленной таблицы</td>
</tr>
<tr>
<td><strong>setup(string $tableName): self</strong></td>
<td>Создает новую кастомную таблицу для атрибутов со всеми необходимыми полями и уникальным индексом</td>
</tr>
</tbody>
</table>

### Методы работы с обычными атрибутами

<table border="1" style="border-collapse: collapse; width: 100%;">
<tbody>
<tr style="background-color: #c2e0f4;">
<td style="width: 50%;">Метод</td>
<td style="width: 50%;">Описание</td>
</tr>
<tr>
<td><strong>setAttr(string $key, mixed $value, ?string $key2 = null, ?string $key3 = null): self</strong></td>
<td>Устанавливает значение обычного атрибута в базе данных</td>
</tr>
<tr>
<td><strong>getAttr(string $key, ?string $key2 = null, ?string $key3 = null): mixed|null</strong></td>
<td>Получает значение обычного атрибута из базы данных</td>
</tr>
<tr>
<td><strong>getIntAttr(string $key, ?int $default = 0, ?string $key2 = null, ?string $key3 = null): int|null</strong></td>
<td>Получает значение атрибута, преобразованное в целое число</td>
</tr>
<tr>
<td><strong>getFloatAttr(string $key, ?float $default = 0.0, ?string $key2 = null, ?string $key3 = null): float|null</strong></td>
<td>Получает значение атрибута, преобразованное в число с плавающей точкой</td>
</tr>
<tr>
<td><strong>getBoolAttr(string $key, ?bool $default = false, ?string $key2 = null, ?string $key3 = null): bool|null</strong></td>
<td>Получает значение атрибута, преобразованное в булево значение</td>
</tr>
<tr>
<td><strong>isAttrExist(string $key, ?string $key2 = null, ?string $key3 = null): bool</strong></td>
<td>Проверяет существование атрибута в базе данных</td>
</tr>
<tr>
<td><strong>issetAttr(string $key, ?string $key2 = null, ?string $key3 = null): bool</strong></td>
<td>Проверяет существование атрибута в базе данных (аналог isAttrExist)</td>
</tr>
<tr>
<td><strong>deleteAttr(string $key, ?string $key2 = null, ?string $key3 = null): self</strong></td>
<td>Удаляет атрибут из базы данных</td>
</tr>
<tr>
<td><strong>getAllAttr(): array</strong></td>
<td>Получает все обычные атрибуты (не JSON) из базы данных</td>
</tr>
</tbody>
</table>

### Методы работы с JSON атрибутами

<table border="1" style="border-collapse: collapse; width: 100%;">
<tbody>
<tr style="background-color: #c2e0f4;">
<td style="width: 50%;">Метод</td>
<td style="width: 50%;">Описание</td>
</tr>
<tr>
<td><strong>setJsonAttr(string $key, mixed $value, ?string $key2 = null, ?string $key3 = null): self</strong></td>
<td>Устанавливает значение JSON атрибута в базе данных. Поддерживает точечную нотацию для вложенных структур</td>
</tr>
<tr>
<td><strong>getJsonAttr(string $key, ?string $key2 = null, ?string $key3 = null): mixed|null</strong></td>
<td>Получает значение JSON атрибута из базы данных. Поддерживает точечную нотацию для извлечения вложенных значений</td>
</tr>
<tr>
<td><strong>isJsonAttrKeyExist(string $key, ?string $key2 = null, ?string $key3 = null): bool</strong></td>
<td>Проверяет существование JSON атрибута в базе данных</td>
</tr>
<tr>
<td><strong>issetJsonAttr(string $key, ?string $key2 = null, ?string $key3 = null): bool</strong></td>
<td>Проверяет существование JSON атрибута в базе данных (аналог isJsonAttrKeyExist)</td>
</tr>
<tr>
<td><strong>deleteJsonAttr(string $key, ?string $key2 = null, ?string $key3 = null): self</strong></td>
<td>Удаляет JSON атрибут из базы данных</td>
</tr>
<tr>
<td><strong>getAllJsonAttrs(): array</strong></td>
<td>Получает все JSON атрибуты из базы данных</td>
</tr>
</tbody>
</table>

---

## Примеры использования

### Пример 1: Базовое использование

```javascript
let customStorage = require('Common.Platform.CustomStorage')

// Устанавливаем таблицу
customStorage.setTableName('business_attributes')

// Сохранение простых атрибутов
customStorage.setAttr('server_name', 'production-server-01')
customStorage.setAttr('server_ip', '192.168.1.100')
customStorage.setAttr('server_port', '8080')

// Получение атрибутов
let serverName = customStorage.getAttr('server_name')
let serverIp = customStorage.getAttr('server_ip')
let port = customStorage.getIntAttr('server_port', 0)

bot.sendText('Сервер: ' + serverName + ' (' + serverIp + ':' + port + ')')
```

### Пример 2: Использование подкатегорий (key2, key3)

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Сохранение данных для разных регионов и датацентров
customStorage.setAttr('server_name', 'server-01', 'us-east', 'dc-01')
customStorage.setAttr('server_ip', '10.0.1.10', 'us-east', 'dc-01')
customStorage.setAttr('server_name', 'server-02', 'us-east', 'dc-02')
customStorage.setAttr('server_ip', '10.0.2.10', 'us-east', 'dc-02')

// Получение данных для конкретного региона и датацентра
let serverName = customStorage.getAttr('server_name', 'us-east', 'dc-01')
let serverIp = customStorage.getAttr('server_ip', 'us-east', 'dc-01')

bot.sendText('Сервер в us-east/dc-01: ' + serverName + ' (' + serverIp + ')')
```

### Пример 3: Работа с JSON атрибутами

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Сохранение простого JSON объекта
customStorage.setJsonAttr('config', {
    timeout: 30,
    retries: 3,
    enabled: true
})

// Получение JSON объекта
let config = customStorage.getJsonAttr('config')
if (config) {
    bot.sendText('Timeout: ' + config.timeout)
    bot.sendText('Retries: ' + config.retries)
    bot.sendText('Enabled: ' + config.enabled)
}
```

### Пример 4: Точечная нотация для вложенных JSON структур

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Установка вложенных значений через точечную нотацию
customStorage.setJsonAttr('user.profile.name', 'John Doe')
customStorage.setJsonAttr('user.profile.email', 'john@example.com')
customStorage.setJsonAttr('user.profile.age', 30)
customStorage.setJsonAttr('user.settings.theme', 'dark')
customStorage.setJsonAttr('user.settings.language', 'ru')

// Получение вложенных значений
let userName = customStorage.getJsonAttr('user.profile.name')
let userEmail = customStorage.getJsonAttr('user.profile.email')
let userTheme = customStorage.getJsonAttr('user.settings.theme')

bot.sendText('Пользователь: ' + userName + ' (' + userEmail + ')')
bot.sendText('Тема: ' + userTheme)

// Получение всего объекта верхнего уровня
let userProfile = customStorage.getJsonAttr('user')
// userProfile будет содержать: { profile: { name: "John Doe", email: "john@example.com", age: 30 }, settings: { theme: "dark", language: "ru" } }
```

### Пример 5: JSON атрибуты с подкатегориями

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Сохранение JSON конфигурации для разных регионов
customStorage.setJsonAttr('server.config', { cpu: 8, ram: 32 }, 'region1', 'dc1')
customStorage.setJsonAttr('server.config', { cpu: 16, ram: 64 }, 'region2', 'dc2')

// Получение конфигурации для конкретного региона
let config1 = customStorage.getJsonAttr('server.config', 'region1', 'dc1')
let config2 = customStorage.getJsonAttr('server.config', 'region2', 'dc2')

if (config1) {
    bot.sendText('Регион 1: CPU=' + config1.cpu + ', RAM=' + config1.ram)
}
if (config2) {
    bot.sendText('Регион 2: CPU=' + config2.cpu + ', RAM=' + config2.ram)
}
```

### Пример 6: Глубокая вложенность JSON

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Установка глубоко вложенных значений
customStorage.setJsonAttr('app.settings.database.host', 'localhost')
customStorage.setJsonAttr('app.settings.database.port', 5432)
customStorage.setJsonAttr('app.settings.database.name', 'myapp')
customStorage.setJsonAttr('app.settings.cache.enabled', true)
customStorage.setJsonAttr('app.settings.cache.ttl', 3600)

// Получение глубоко вложенных значений
let dbHost = customStorage.getJsonAttr('app.settings.database.host')
let dbPort = customStorage.getJsonAttr('app.settings.database.port')
let cacheEnabled = customStorage.getJsonAttr('app.settings.cache.enabled')

bot.sendText('DB Host: ' + dbHost)
bot.sendText('DB Port: ' + dbPort)
bot.sendText('Cache Enabled: ' + cacheEnabled)
```

### Пример 7: Работа с массивами

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Сохранение массива
customStorage.setJsonAttr('packages', [
    { name: 'nginx', version: '1.18.0' },
    { name: 'php', version: '8.1.0' },
    { name: 'mysql', version: '8.0.0' }
])

// Получение массива
let packages = customStorage.getJsonAttr('packages')
if (packages && Array.isArray(packages)) {
    bot.sendText('Установлено пакетов: ' + packages.length)
    packages.forEach(function(pkg) {
        bot.sendText('- ' + pkg.name + ' v' + pkg.version)
    })
}
```

### Пример 8: Проверка существования и удаление

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Проверка существования
if (customStorage.isAttrExist('server_name')) {
    let serverName = customStorage.getAttr('server_name')
    bot.sendText('Сервер найден: ' + serverName)
} else {
    bot.sendText('Сервер не найден')
}

// Проверка JSON атрибута
if (customStorage.isJsonAttrKeyExist('user.profile.name')) {
    let userName = customStorage.getJsonAttr('user.profile.name')
    bot.sendText('Имя пользователя: ' + userName)
}

// Удаление атрибутов
customStorage.deleteAttr('server_port')
customStorage.deleteJsonAttr('config')

// Удаление с подкатегориями
customStorage.deleteAttr('server_name', 'region1', 'dc1')
customStorage.deleteJsonAttr('server.config', 'region1', 'dc1')
```

### Пример 9: Массовые операции

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Получение всех обычных атрибутов
let allAttrs = customStorage.getAllAttr()
bot.sendText('Всего обычных атрибутов: ' + Object.keys(allAttrs).length)

// Получение всех JSON атрибутов
let allJsonAttrs = customStorage.getAllJsonAttrs()
bot.sendText('Всего JSON атрибутов: ' + Object.keys(allJsonAttrs).length)
```

---

## Важные замечания

### Безопасность

- Плагин работает только с кастомными таблицами текущего бизнеса
- Все операции проверяют принадлежность таблицы к текущему бизнесу
- При попытке доступа к таблице другого бизнеса будет выброшена ошибка

### Уникальность записей

- В таблице создается уникальный индекс на комбинацию `(key, key2, key3)`
- При попытке установить значение для существующей комбинации `(key, key2, key3)` запись будет обновлена
- Если в базе данных найдено несколько записей для одной комбинации `(key, key2, key3)`, будет выброшена ошибка с просьбой вручную разрешить дубликаты

### Работа с памятью

- Плагин работает напрямую с базой данных без кэширования в памяти
- Все операции чтения и записи выполняются непосредственно в БД
- Это обеспечивает актуальность данных, но может быть медленнее при частых обращениях

### Точечная нотация для JSON

- При использовании точечной нотации (например, `user.profile.name`) верхний ключ (`user`) используется как `key` в БД
- Остальные части пути (`profile`, `name`) создают вложенную структуру в JSON объекте
- При установке нескольких значений с одним верхним ключом они объединяются в один JSON объект

### Подкатегории key2 и key3

- `key2` и `key3` используются для группировки данных и не связаны с вложенностью JSON
- Они позволяют хранить разные значения для одного и того же `key` в разных контекстах
- Например, можно хранить конфигурацию сервера для разных регионов: `server.config` с `key2='region1'`, `key3='dc1'`

---

## Обработка ошибок

Все методы плагина могут выбрасывать исключения типа `UnauthotizedV8Exception` в следующих случаях:

- Таблица не найдена
- Таблица не принадлежит текущему бизнесу
- Найдено несколько записей для одной комбинации `(key, key2, key3)`
- Ошибка при работе с базой данных

**Важно:** В V8Js невозможно отлавливать исключения бэкенда через `try/catch` в JavaScript. Поэтому рекомендуется использовать специальные методы проверки существования атрибутов перед их использованием:

### Проверка существования обычных атрибутов

Для проверки существования обычных атрибутов используйте методы `isAttrExist()` или `issetAttr()`:

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Безопасное получение атрибута с проверкой существования
if (customStorage.isAttrExist('server_name')) {
    let value = customStorage.getAttr('server_name')
    bot.sendText('Значение: ' + value)
} else {
    bot.sendText('Атрибут не найден')
}

// Альтернативный вариант с issetAttr()
if (customStorage.issetAttr('server_name')) {
    let value = customStorage.getAttr('server_name')
    bot.sendText('Значение: ' + value)
}

// С подкатегориями
if (customStorage.isAttrExist('server_name', 'region1', 'dc1')) {
    let value = customStorage.getAttr('server_name', 'region1', 'dc1')
    bot.sendText('Сервер в регионе 1: ' + value)
}
```

### Проверка существования JSON атрибутов

Для проверки существования JSON атрибутов используйте методы `isJsonAttrKeyExist()` или `issetJsonAttr()`:

```javascript
let customStorage = require('Common.Platform.CustomStorage')
customStorage.setTableName('business_attributes')

// Проверка простого JSON атрибута
if (customStorage.isJsonAttrKeyExist('config')) {
    let config = customStorage.getJsonAttr('config')
    bot.sendText('Timeout: ' + config.timeout)
}

// Проверка вложенного JSON атрибута с точечной нотацией
if (customStorage.isJsonAttrKeyExist('user.profile.name')) {
    let userName = customStorage.getJsonAttr('user.profile.name')
    bot.sendText('Имя пользователя: ' + userName)
}

// Альтернативный вариант с issetJsonAttr()
if (customStorage.issetJsonAttr('user.profile.email')) {
    let email = customStorage.getJsonAttr('user.profile.email')
    bot.sendText('Email: ' + email)
}

// С подкатегориями
if (customStorage.isJsonAttrKeyExist('server.config', 'region1', 'dc1')) {
    let config = customStorage.getJsonAttr('server.config', 'region1', 'dc1')
    bot.sendText('CPU: ' + config.cpu + ' cores')
}
```

**Рекомендация:** Всегда используйте методы проверки существования (`isAttrExist()`, `issetAttr()`, `isJsonAttrKeyExist()`, `issetJsonAttr()`) вместо проверки на `null` после вызова `getAttr()` или `getJsonAttr()`. Это более явный и безопасный способ проверки.

---

## Сравнение с атрибутами лидов

Плагин `CustomStorage` предоставляет аналогичный функционал, что и работа с атрибутами лидов через `lead.setAttr()` и `lead.getAttr()` и т.д., но с дополнительными возможностями:

| Функция | lead | customStorage |
|---------|------|-------------------|
| Работа с обычными атрибутами | ✅ | ✅ |
| Работа с JSON атрибутами | ✅ | ✅ |
| Точечная нотация для JSON | ✅ | ✅ |
| Подкатегории (key2, key3) | ❌ | ✅ |
| Работа с кастомными таблицами | ❌ | ✅ |
| Прямая работа с БД | ❌ | ✅ |

---

## Заключение

Плагин `CustomStorage` предоставляет мощный инструмент для работы с кастомными таблицами атрибутов в JavaScript скриптах. Он позволяет гибко хранить и получать данные с поддержкой подкатегорий и вложенных JSON структур, что делает его незаменимым инструментом для сложных сценариев работы с данными.

# Инструкция по подключению Google Scripts

### Настройка Google Scripts (Не будет ли проблем из-за копирования)

1. Создаём и настраиваем Google Sheets
2. Переходим в Google Scripts через вкладку “Расширения”

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-02/scaled-1680-/Rz8BC51WgjhEieCl-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-02/Rz8BC51WgjhEieCl-image.png)

1. Копируем и вставляем этот скрипт в редактор Google Scripts
    
    [Google scripts.txt](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/275a98e7-9751-495e-996c-cd2d93d07377/Google_scripts.txt)
2. Нажимаем на иконку “Сохранить”, а за тем “Выполнить”

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-02/scaled-1680-/45qO05V3lN0NRqOL-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-02/45qO05V3lN0NRqOL-image.png)

В первый раз приложение запросит доступы к Google аккаунту, нужно всё разрешить.

1. Нажимаем на кнопку “Начать развёртывание” &gt;&gt; “Новое развёртывание”

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-02/scaled-1680-/Bmx0QC9lkawlj4jQ-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-02/Bmx0QC9lkawlj4jQ-image.png)

1. Выбираем тип “Веб приложение” &gt;&gt; У кого есть доступ “Все” &gt;&gt; Начать развёртывание

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-02/scaled-1680-/akwflWxpXCRQdeQa-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-02/akwflWxpXCRQdeQa-image.png)

1. Копируем идентификатор развёртывания.

[![image.png](https://docs.metabot24.ru/uploads/images/gallery/2023-02/scaled-1680-/uPHeTjU0tM3ociJL-image.png)](https://docs.metabot24.ru/uploads/images/gallery/2023-02/uPHeTjU0tM3ociJL-image.png)

### Настройка плагина в Metabot

- Переходим в Metabot и в атрибут бота “gs\_data\_studio\_token” вставляем скопированное значение
- Атрибут бота “gs\_event\_id” прописываем последнее значение из таблицы. Если его нет, то прописываем “0”. Если вы забудете прописать этот атрибут, то скрипт присвоит “0” автоматически

### Подключение плагина и вызов методов

**Для подключения плагина нужно использовать вот такой код:**

```jsx
snippet('Business.DataStudio.LogEvent'); // Вызываем плагин бизнеса
```

После этого вызываем необходимые для нас функции.

**Если нужно добавить пользователя:**

```jsx
snippet('Business.DataStudio.LogEvent'); // Вызываем плагин бизнеса
AddUser(user_id, user, subscription_datetime); // Добавляем нового пользователя
```

**Если нам нужно обновить данные о пользователе**

```jsx
snippet('Business.DataStudio.LogEvent'); // Вызываем плагин бизнеса
UpdateUser(user_id, user, subscription_datetime); // Обновляем данные о пользователе
```

---

<span class="notion-enable-hover" data-token-index="0">Если нужно записать событие, в котором участвует пользователь</span>

```
snippet('Business.DataStudio.LogEvent'); // Вызываем плагин бизнеса
NewEvent(user_id, type_name, contest_id, event_type_id,	task_id, event_datetime);
```

<table border="1" id="bkmrk-user_id-id-%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82" style="border-collapse: collapse; width: 100%; height: 178.781px;"><tbody><tr style="height: 29.7969px;"><th style="width: 24.9691%;">user\_id</th><th style="width: 24.9691%;">id пользователя (Тот же что выбрали при добавлении пользователя)</th><th style="width: 24.9691%;">integer</th><th style="width: 24.9691%;">Опционально</th></tr><tr style="height: 29.7969px;"><td style="width: 24.9691%;">type\_name</td><td style="width: 24.9691%;">Название конкурса</td><td style="width: 24.9691%;">String</td><td style="width: 24.9691%;">Обязательно</td></tr><tr style="height: 29.7969px;"><td style="width: 24.9691%;">contest\_id</td><td style="width: 24.9691%;">id конкурса</td><td style="width: 24.9691%;">integer</td><td style="width: 24.9691%;">Опционально</td></tr><tr style="height: 29.7969px;"><td style="width: 24.9691%;">event\_type\_id</td><td style="width: 24.9691%;">id типа события</td><td style="width: 24.9691%;">integer</td><td style="width: 24.9691%;">Опционально</td></tr><tr style="height: 29.7969px;"><td style="width: 24.9691%;">task\_id</td><td style="width: 24.9691%;">id задачи</td><td style="width: 24.9691%;">integer</td><td style="width: 24.9691%;">Опционально</td></tr><tr style="height: 29.7969px;"><td style="width: 24.9691%;">event\_datetime</td><td style="width: 24.9691%;">Дата/время наступления события</td><td style="width: 24.9691%;">String</td><td style="width: 24.9691%;">Опционально</td></tr></tbody></table>

# Документация по Notifier

Модуль **Notifier** предназначен для отправки уведомлений в Telegram-чаты, например, о событиях, ошибках или статусах работы системы. Используется для оперативного информирования администраторов и команд.

---

### Конфигурация

- **chat\_ids** — объект с идентификаторами чатов, берётся из атрибута `plg_notifier_chat_ids`: 
    - Пример: `{ "default": -123456789, "admins": -987654321 }`

---

### Методы

### sendToTelegramChat(message, jsonParams, groupCode, type)

Отправляет сообщение в указанный Telegram-чат.

**Параметры**

<table id="bkmrk-%D0%98%D0%BC%D1%8F-%D0%A2%D0%B8%D0%BF-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-mes"><thead><tr><th>Имя</th><th>Тип</th><th>Описание</th></tr></thead><tbody><tr><td>message</td><td>String</td><td>Текст сообщения (обязательно)</td></tr><tr><td>jsonParams</td><td>Object</td><td>Дополнительные параметры, выводятся как JSON-блок (опционально)</td></tr><tr><td>groupCode</td><td>String</td><td>Код группы чата, например "default", "admins" (по умолчанию "default")</td></tr><tr><td>type</td><td>String</td><td>Тип сообщения: "info", "success", "warning", "error", "debug" (по умолчанию "info")</td></tr></tbody></table>

**Возвращает**  
Boolean — `true` если успешно, `false` если ошибка.

**Пример:**

```js
Notifier.sendToTelegramChat(
  "Выполнена операция", 
  { user: "admin", status: "ok" }, 
  "admins", 
  "success"
)
```

---

### Формат сообщения

- В начале сообщения добавляется иконка по типу: 
    - info: ℹ️
    - success: ✅
    - warning: ⚠️
    - error: ❌
    - debug: 🔧
- Если переданы параметры, они выводятся в виде форматированного JSON-блока.
- Используется Markdown для форматирования.

---

### Обработка ошибок

- Если сообщение пустое или не строка — возвращает `false` и пишет ошибку в лог.
- Если группа чата не найдена — возвращает `false` и пишет ошибку в лог.
- Если ошибка при отправке — возвращает `false` и пишет ошибку в лог.

---

### Быстрый старт

1. Настройте атрибут `plg_notifier_chat_ids` с нужными чатами.
2. Все уведомления из плагинов LLM уже приходят в ваш чат.
3. Или вручную используйте `Notifier.sendToTelegramChat()` для отправки уведомлений.

# Common.Observability.* — Инструменты наблюдаемости

Observability — пакет системных инструментов для наблюдаемости, диагностики и эскалации событий в Metabot. Предназначен для фиксации, анализа и понимания поведения системы без влияния на бизнес-логику.

# Common.Observability.Tracer — Универсальный трассировщик событий

**Автор:** Art Yg

Tracer предназначен для записи любых диагностических событий в таблицы базы данных:

* ошибок
* проверок
* ветвлений логики
* внутренних состояний
* отладочной информации

Tracer **не знает**:

* что такое сессия
* что такое скрипт, команда или триггер
* какие поля считаются «правильными»

Он записывает **ровно те данные, которые ему передали**.

---

## Основные принципы

* **Stateless** — не хранит состояние
* **Schema-agnostic** — не требует фиксированной схемы
* **Zero mandatory fields** — нет обязательных полей
* **Opt-in** — работает только если включён
* **Side-effect only** — не влияет на выполнение кода

Tracer можно удалить из проекта — бизнес-логика продолжит работать.

---

## Минимальные требования

Для начала работы достаточно:

1. Создать любую таблицу в БД
   (даже без полей, кроме `id`)
2. Добавить атрибут бота `TRACER_CONFIG`
3. Вызвать `trace()`

Рекомендуемое, но **не обязательное** поле таблицы:

* `created_at` с автозаполнением (`NOW`)

Tracer **не управляет временем**.
Если поле есть — БД заполнит его автоматически.
Если нет — запись всё равно создаётся.

---

## Конфигурация

Tracer настраивается через один JSON в атрибутах бота: `TRACER_CONFIG`.

```json
{
  "navigation": {
    "enabled": true,
    "table": "nav_trace"
  },
  "ai": {
    "enabled": false,
    "table": "ai_trace"
  }
}
```

* каждый tracer имеет имя (`navigation`, `ai`, `api` и т.д.)
* у каждого tracera своя таблица (или общая)
* выключенный tracer ничего не пишет

---

## Использование

```js
const Tracer = require("Common.Observability.Tracer");

Tracer.trace("navigation", {
  category: "NAVIGATION",
  component: "Actor",
  action: "hasAchievement",
  level: "OK",
  payload: {
    actor_id: 42,
    achievement: "first_step",
    result: true
  }
});
```

Если tracer выключен — метод молча завершится.

---

## Методы Tracer

Tracer предоставляет несколько эквивалентных методов:

* `trace(name, data)` — базовый метод записи
* `log(name, data)` — алиас для читаемости
* `info(name, data)` — добавляет `level: "INFO"`
* `error(name, data)` — добавляет `level: "ERROR"`

Все методы:

* не выбрасывают ошибок
* не изменяют переданные данные
* не влияют на бизнес-логику

---

## Данные события

Tracer принимает **любой объект**.

Все поля:

* опциональны
* именуются произвольно
* записываются «как есть»

Рекомендуемые (но не обязательные):

* `category` — область (NAVIGATION, AI, API)
* `component` — компонент
* `action` — действие
* `source` — источник (system, user, webhook)
* `level` — уровень ошибки
* `payload` — любые данные (тип поля TEXTAREA)

Если таблица не содержит поле — БД вернёт ошибку.
В таком случае используйте `payload`.

---

## Работа со временем

Tracer:

* не добавляет timestamp
* не требует поля времени
* позволяет передать своё время

```js
{
  event_time: "2026-01-16T12:00:00Z"
}
```

или

```js
{
  created_at: "2026-01-16T12:00:00Z"
}
```

---

## Когда использовать

* метод возвращает `true / false`, но нужна диагностика
* не хочется усложнять ответы ошибками
* важно понять, **почему** логика не сработала
* нужна отладка без влияния на сценарии

---

## Когда не использовать

* как бизнес-лог
* как аудит-лог
* как аналитику или метрики

---

## Итог

`Common.Observability.Tracer` — простой и ненавязчивый способ видеть, что происходит внутри системы.

Никакой магии.
Никаких обязательств.
Никакой боли.

---

# Версия 1.1 — Интеграция с Incident

В версии **1.1** Tracer получил **опциональную интеграцию с системой инцидентов**.

Tracer по-прежнему:

* не знает, что такое уведомления;
* не содержит логики доставки;
* не требует дополнительных обязательных полей.

Интеграция включается **исключительно через конфигурацию**.

---

## Что добавлено

Tracer теперь может:

* автоматически инициировать **Incident** при записи события уровня `ERROR`;
* делать это **без изменения существующих вызовов** `Tracer.error()` и `Tracer.trace()`;
* работать с инцидентами как с **побочным эффектом**, не влияя на основной код.

Если интеграция не настроена — Tracer ведёт себя **точно так же, как в версии 1.0**.

---

## Расширенная конфигурация

В `TRACER_CONFIG` можно указать блок `incident` для любого tracera:

```json
{
  "navigation": {
    "enabled": true,
    "table": "nav_trace",
    "incident": {
      "enabled": true,
      "type": "navigation_failed",
      "severity": "error"
    }
  }
}
```

### Поведение:

* `incident.enabled = true` — включает обработку инцидентов
* `type` — логический тип инцидента (используется Incident)
* `severity` — уровень инцидента (опционально)

Инцидент инициируется **только если**:

* tracer включён;
* событие имеет `level = ERROR` (регистр не важен).

---

## Архитектурные гарантии

Интеграция с Incident:

* **никогда не ломает выполнение сценария**;
* **не выбрасывает исключений**;
* **не требует изменений в сценариях**;
* полностью **отключается конфигурацией**.

Tracer остаётся:

* stateless,
* schema-agnostic,
* opt-in,
* side-effect only.

---

## Использование (без изменений)

```js
Tracer.error("navigation", {
  component: "Reflection",
  action: "build",
  payload: {
    reason: "profile_missing"
  }
});
```

Если Incident включён — будет создан инцидент.
Если нет — только запись в таблицу.

---

## Итог версии 1.1

Версия **1.1** добавляет Tracer’у способность **сигналить о сбоях**,
не превращая его в логгер, нотификатор или бизнес-модуль.

Tracer по-прежнему ничего не «решает».
Он просто сообщает — системе, а не человеку.

---

# Common.Observability.Incident — Централизованный обработчик инцидентов

## Common.Observability.Incident

**Incident** — централизованный обработчик инцидентов в Metabot.
Предназначен для регистрации и уведомления о сбоях, ошибках и критических состояниях системы.

Класс **не содержит бизнес-логики** и не влияет на выполнение сценариев.
Он используется как реакция на события (например, из Tracer) или вызывается напрямую из сценариев.

---

### Назначение

Incident решает одну задачу:
**превратить техническое событие в уведомление для команды.**

Типичные случаи:

* сбой профилирования;
* ошибка отражения / навигации;
* неконсистентное состояние данных;
* критические ошибки AI / runtime.

---

### Уведомления

В текущей реализации Incident использует Telegram через плагин:

```
Common.Notifications.Telegram
```

Для работы требуются **два атрибута бота**:

* `SUPPORT_TELEGRAM_BOT_TOKEN` — токен Telegram-бота
* `SUPPORT_TELEGRAM_CHAT_ID` — ID группы или канала для уведомлений

Incident сам не хранит токены и не управляет доступами.

---

### Шаблоны сообщений

Тексты уведомлений настраиваются через атрибут бота
`INCIDENT_TEMPLATES` (JSON).

Пример:

```json
{
  "profiling_failed": {
    "ru": {
      "title": "🧭 ORION · Сбой профилирования",
      "body": [
        "Профиль не был корректно сформирован.",
        "",
        "Lead ID: {{lead_id}}",
        "{{error}}"
      ]
    }
  }
}
```

Поддерживаются:

* разные типы инцидентов;
* несколько языков;
* плейсхолдеры `{{variable}}`.

---

### Использование

Incident может вызываться:

* напрямую из сценариев;
* автоматически из `Common.Observability.Tracer` (при включённой конфигурации).

Это позволяет централизовать обработку ошибок **без дублирования кода уведомлений**.

---

**Incident — это точка ответственности за инциденты,
а не ещё один логгер или бизнес-модуль.**

# Common.Platform.* — Платформенные примитивы исполнения

Пакет системных плагинов, формирующих основу исполнения, контекста и жизненного цикла диалогов и процессов внутри платформы.

Этот пакет содержит платформенные примитивы, которые:

* управляют состоянием выполнения,
* хранят и передают контекст,
* используются guard’ами, input-компонентами и системными проверками,
* не зависят от конкретных сценариев, каналов или бизнес-доменов.

Platform не является:
— бизнес-логикой,
— сценарным фреймворком,
— workflow-движком,
— пользовательским интерфейсом,
— конкретным каналом ввода (voice / text / ui).

Platform — это инфраструктурный слой, на котором строятся остальные механизмы системы.

# Common.Platform.AsyncFallback — Универсальный helper для таймаутов асинхронных операций

**Автор:** Art Yg  
**Версия:** 1.0

`AsyncFallback` — это платформенный helper, который решает одну практическую проблему:  
**как безопасно обработать ситуацию “мы отправили async-запрос, но ответ может не прийти вовремя или вообще не прийти”**.

Он нужен для любых операций, где есть “PHASE 1 → отправили запрос” и “PHASE 2 → пришёл callback”:

- LLM (Remote LLM Query)
- ImageGen (генерация изображений)
- STT / Voice Transcription
- любые RemoteApiCall (внешние API, webhook processor, интеграции)

`AsyncFallback` **не является AI-модулем**. Это оркестрация платформенного уровня.

---

## Зачем он существует

В реальном мире async-вызовы ломаются не потому что “код плохой”, а потому что:

- провайдер завис / долго отвечает
- callback потерялся на транспорте
- webhook processor упал
- ответ пришёл, но неполный (нарушение контракта)
- пользователь продолжает писать, пока операция ещё не завершилась

Без таймаута сценарий может **залипнуть**: пользователь пишет, а система “ждёт” бесконечно.

`AsyncFallback` решает это через стандартный механизм Metabot Scheduler:

1. **Планирует job**: “через N секунд выполнить fallback-script”
2. **Отменяет job**, когда callback успешно пришёл
3. Позволяет фиксировать **причину ошибки** и **детали**, чтобы дальше можно было принять решение (редирект, retry, сообщение пользователю)

---

## Основные принципы

- **Platform-layer** — не привязан к AI, применяется везде
- **Namespace-based** — несколько параллельных async-операций не конфликтуют
- **Opt-in** — если timeout не задан, ничего не планируется
- **Side-effect only** — не вмешивается в бизнес-логику, только планирует/снимает job и пишет маркеры
- **Debug-friendly** — сохраняет конфиг и last_reason/last_details в lead (по namespace)

---

## Минимальные требования

Чтобы использовать `AsyncFallback`, нужно:

1. Иметь доступ к **планировщику** Metabot:
   - `bot.scheduleJob({ lead_id, script_code, run_after_sec })`
   - `bot.clearJobsByScriptCode(script_code, lead_id)`
2. Иметь `leadId` (или возможность вычислить lead_id)

`AsyncFallback` не требует базы данных, таблиц или дополнительных сервисов.

---

## Концепция Namespace

**Namespace** — обязательный “scope” для идентификации конкретной операции.

Это важно, потому что на одном lead могут одновременно идти:

- ImageGen (ожидаем картинку)
- LLMQuery (ожидаем JSON)
- STT (ожидаем текст транскрипции)

Если бы мы хранили “timeout.script” без namespace — они бы перетирали друг друга.

Пример namespace:

- `orion_image_reflection`
- `llm_actor_profile_extract`
- `voice_transcription_q1`

---

## Конфигурация

`AsyncFallback` конфигурируется на вызове:

- `namespace` — обязательно
- `timeout` — опционально
- `error` — опционально
- `storageRoot` — почти всегда не трогаем

### Поля конфигурации

- `timeout.seconds` — через сколько секунд считать операцию “просроченной”
- `timeout.script` — какой скрипт выполнить по истечении таймаута
- `error.flagAttr` — атрибут флага ошибки на lead (например `true/false`)
- `error.reasonAttr` — атрибут причины ошибки (строка)

---

## Как использовать

### PHASE 1 — отправка async-запроса (isFirstImmediateCall)

1) Конфигурируем fallback  
2) Планируем таймаут  
3) Отправляем remote request  
4) Возвращаем `false` (ждём callback)

```js
const AsyncFallback = require("Common.Platform.AsyncFallback");

const fb = AsyncFallback.configure({
  lead,
  namespace: "orion_image_reflection",
  timeout: { seconds: 120, script: "Orion_Image_Timeout" },
  error: { flagAttr: "orion_image_error", reasonAttr: "orion_image_error_reason" }
});

fb.schedule();

// ... RemoteApiCall.send(..., asyncResponse: true)
return false;
```

---

### PHASE 2 — пришёл callback

1) Снимаем таймаут (если он был)  
2) Валидируем результат  
3) Если контракт нарушен — `fail(reason, details)`  
4) Дальше ты решаешь: редиректить в error-script или вернуться “в ту же точку”

```js
const AsyncFallback = require("Common.Platform.AsyncFallback");

const fb = AsyncFallback.configure({
  lead,
  namespace: "orion_image_reflection"
});

fb.unschedule();

// если ответ плохой (например нет url при requireUrl=true)
fb.fail("url_missing", { requireUrl: true });

// твоя стратегия выхода:
return bot.run({ script_code: "Orion_Image_Error" });
```

---

## Методы

### `AsyncFallback.configure(params) → instance`

Создаёт instance и **сохраняет конфиг** в lead под namespace.  
Главная точка входа. Используй и в PHASE 1, и в PHASE 2 — единообразно.

---

### `instance.schedule() → boolean`

Планирует fallback-job, если `timeout.seconds` и `timeout.script` заданы.

Поведение:

- если timeout не задан → возвращает `false`, не считается ошибкой
- перед планированием **снимает предыдущие jobs** этого script_code для lead (защита от дублей)
- выставляет служебный флаг `active=1`

---

### `instance.unschedule() → boolean`

Отменяет запланированный fallback-job (если он был).

Поведение:

- если script неизвестен → `false`
- снимает `active=0` даже если clearJobs вернул false (чтобы состояние не “залипало”)

---

### `instance.fail(reason, details?) → true`

Фиксирует ошибку и причину:

- `error.flagAttr = true` (если задан)
- `error.reasonAttr = reason` (если задан)
- дополнительно пишет namespace-атрибуты:
  - `last_reason`
  - `last_details` (JSON/string)

Это **не редирект** и **не exception** — это маркер, после которого ты сам решаешь, что делать.

---

### `instance.clear() → true`

Очищает служебные данные namespace:

- config
- script/seconds/active
- last_reason/last_details

Полезно после успешного завершения операции (опционально).

---

## Типовой паттерн: “пользователь пишет, пока ждём”

`AsyncFallback` — про таймаут, но он закрывает важный кусок UX:

- если пользователь продолжает писать, пока async-операция ещё ждёт callback,
  ты показываешь “⏳ ждите…”
- если callback так и не пришёл — fallback-job переведёт сценарий в timeout-script

Обычно это делается так:

- **проверяешь** `payload.is_async_response`
- если это не callback — отправляешь `processing` и возвращаешь `false`
- callback → `unschedule()`

(Эта логика живёт в конкретном плагине типа ImageGen/LLMQuery, а AsyncFallback даёт им общий таймаутный механизм.)

---

## Когда использовать

- есть async callback и риск “зависнуть”
- важно гарантировать, что сценарий не будет ждать бесконечно
- нужен единый механизм таймаута для разных компонентов
- хочешь стандартизировать “timeout-script” как часть контракта компонента

---

## Когда не использовать

- операция строго синхронная
- у операции нет понятного “deadline” (таймаут не имеет смысла)
- ты уже используешь другой механизм оркестрации таймаутов, и второй будет конфликтовать

---

## Практические замечания, чтобы не словить редкий ад

### Поздний callback после таймаута

Может случиться: таймаут-скрипт уже отработал, а потом всё же прилетел success-callback.  
`AsyncFallback` снимает job на callback, но **если job уже выполнился**, снять уже нечего.

Правильный паттерн на уровне компонента:

- timeout-script ставит флаг `*_timed_out = true`
- callback-обработчик проверяет флаг и **игнорирует поздний успех** (или делает компенсацию)

Это не обязанность `AsyncFallback`, потому что стратегия зависит от бизнес-логики.

---

## Итог

`Common.Platform.AsyncFallback` — простой, но критически полезный слой платформенной оркестрации:

- даёт **гарантию выхода** из ожидания
- позволяет вести **несколько async-операций параллельно**
- стандартизирует **timeout и error markers**
- не превращается в “монолитный менеджер всего” — остаётся лёгким helper’ом

# Common.AI.* — Работа с AI-сервисами

Пакет плагинов для выполнения запросов к AI-сервисам
(LLM, генерация изображений, речь и т.д.).

Содержит:

* клиентов и запросы к AI-провайдерам;
* асинхронную обработку ответов;
* таймауты и ошибки;
* сохранение и нормализацию результатов.

Пакет не содержит логики принятия решений
и не является агентным или сценарным слоем.

AI здесь — это внешний сервис, а не субъект.

# Common.AI.ImageGen — Генерация изображений

**Автор:** Art Yg  
**Версия:** 1.0

`ImageGen` — универсальный плагин для **асинхронной генерации изображений** через внешний Webhook Processor, с сохранением результата в lead (URL и/или base64) и поддержкой сценарного выхода (success/error/timeout).

Плагин спроектирован так, чтобы работать в **двухфазном режиме**, как `LLMQuery`:

* **PHASE 1** — отправить запрос (async) и выйти из скрипта
* **PHASE 2** — обработать callback-ответ от процессора и продолжить сценарий

---

## Зачем он существует

В Metabot-сценариях нельзя считать генерацию изображения “быстрой синхронной функцией”:

* внешние провайдеры отвечают с задержкой
* ответ может потеряться
* пользователь может продолжать писать, пока мы ждём
* результат может прийти не в том формате (URL vs b64)
* иногда важен **жёсткий контроль ожидания** (timeout), иначе сценарий “повиснет” навсегда

`ImageGen` стандартизирует этот поток:

* отправляет запрос **через Webhook Processor** (Remote transport)
* ждёт callback и распознаёт “это callback или пользовательский ввод”
* сохраняет результат в атрибуты lead
* может уйти в `successScript` (опционально)
* может уйти в `error.script` / `timeout` (если настроены)

---

## Что использует внутри

`ImageGen` — это обёртка, которая опирается на инфраструктурные компоненты:

* **Common.Remote.RemoteApiCall** — транспорт, отправляет запрос в Webhook Processor и включает `asyncResponse`
* **Webhook Processor** — внешний слой, который делает реальный HTTP-запрос к провайдеру и возвращает callback
* **Common.Platform.AsyncFallback** — таймаут/фоллбек-оркестрация (используется внутри `ImageGen` через `timeout`)
* **Common.AI.Prompts** — компонент резолвинга промптов из таблицы и сборки массивов `system/user`, включая ссылки вида `$alias`
* **локальная таблица провайдеров внутри ImageGen** — mapping baseUrl/endpoint/method

Важно: `RemoteApiCall` как транспорт **в принципе поддерживает любых провайдеров**, но текущая версия `ImageGen` по умолчанию заточена под OpenAI Images API.

---

## Поддерживаемые провайдеры

На текущий момент поддержан:

* `openai`

Если вам нужен другой провайдер (например, Replicate, Stability, Midjourney proxy, внутренний сервис) — **свяжитесь с командой**, и мы расширим плагин.

> Примечание по архитектуре: сейчас таблица провайдеров (`PROVIDERS`) находится **внутри плагина**. При необходимости её можно вынести наружу (в конфиг бота/таблицу/отдельный реестр), чтобы вы могли подключать свои провайдеры без изменения кода плагина.

---

## Что сохраняет

В зависимости от настроек:

* `save.urlAttr` — URL изображения (если провайдер вернул url)
* `save.b64Attr` — base64 JSON (`b64_json`), если пришёл именно он
* `save.rawJsonAttr` — сырой payload ответа (для диагностики)

---

## Двухфазный протокол выполнения

### PHASE 1 — отправка запроса

Когда `isFirstImmediateCall = true`:

1. Инициализирует timeout-policy через `Common.Platform.AsyncFallback` (если задан `timeout`)
2. Берёт токен из атрибута бота (по `auth.tokenKey`)
3. Собирает итоговый prompt:

   * если задан `prompts` — собирает `system[] + user[]`
   * элементы массива могут быть:

     * inline строка
     * ссылка `$alias` (берётся из таблицы `promptTable` для `agentName`)
   * если в `prompts` используются `$alias`, то **обязательны** `agentName` и `promptTable`
4. Формирует request body под `/images/generations`
5. Отправляет запрос через `RemoteApiCall.send(..., asyncResponse: true)`
6. (опционально) показывает `messages.wait`
7. Возвращает `false` — сценарий “выходит” и ждёт callback

---

### PHASE 2 — обработка callback

Когда `isFirstImmediateCall = false`:

1. Проверяет, что это действительно callback от процессора (`payload.is_async_response`)
2. Если это не callback (пользователь что-то написал):

   * показывает `messages.processing`
   * возвращает `false`
3. Если callback:

   * снимает таймаут-job через `AsyncFallback.unschedule()`
   * парсит `payload.content`
   * извлекает `url` или `b64_json`
   * сохраняет в lead
   * если включён `requireUrl` и url нет → ошибка
4. Если задан `successScript` — делает `bot.run(successScript)`, иначе возвращается `true`

---

## Конфигурация

Ниже описаны ключевые блоки `ImageGen.run()`.

---

### Provider + Auth

```js
provider: "openai",
auth: { tokenKey: "OPENAI_API_KEY" }
```

* `provider` — ключ провайдера (сейчас актуально `openai`)
* `auth.tokenKey` — имя атрибута бота, где лежит API ключ

---

### Prompts (таблица + массивы)

`ImageGen` поддерживает интерфейс промптов “на вырост” — как в LLMQuery: массивы промптов и табличные ссылки.

```js
agentName: "orion",
promptTable: "gpt_prompts",

prompts: {
  system: ["$avatar_brief_generator"],
  user: ["...основной запрос..."]
}
```

**Принципы:**

* `prompts.system` и `prompts.user` — **массивы строк**.
* элементы массива могут быть:

  * обычной строкой (inline prompt)
  * ссылкой вида `$alias` — тогда текст берётся из `promptTable` для указанного `agentName`
* если вы используете `$alias`, то:

  * `agentName` **обязателен**
  * `promptTable` **обязателен**
  * иначе это конфигурационная ошибка (плагин бросает `throw`, чтобы не было “тихих” генераций без системного слоя)

> Примечание: OpenAI Images API принимает один `prompt` (строкой), поэтому `ImageGen` **склеивает массивы** в итоговый текст (обычно `system` + пустая строка + `user`). Это сделано для унификации с LLMQuery и поддержки других провайдеров/форматов в будущем.

---

### Image параметры

```js
image: {
  model: "dall-e-3",
  n: 1,
  size: "1024x1792",
  quality: "standard",
  style: "natural",
  background: null,
  response_format: "url"
}
```

Практика по форматам результата:

* **dall-e-2 / dall-e-3**: можно запросить `response_format: "url"` и получить временный URL
* **gpt-image-* модели**: часто отдают `b64_json` (URL может не прийти)

---

### requireUrl

```js
requireUrl: true
```

Если `true`, то отсутствие URL считается ошибкой (даже если пришёл b64).

Это удобно для MVP-потока “дай URL, а скачивание/сохранение сделаю потом”.

---

### Таймаут (fallback)

```js
timeout: {
  seconds: 120,
  script: "Orion_Image_Timeout"
}
```

Если callback **не пришёл** за указанное время — планировщик Metabot запускает `timeout.script`.

Таймаут реализован через `Common.Platform.AsyncFallback` **внутри** `ImageGen`.

---

### Namespace для fallback

```js
fallback: {
  namespace: "orion_image_reflection"
}
```

Нужен, чтобы несколько асинхронных операций не конфликтовали.

Если не задан, будет auto:

* `imagegen_${code.toLowerCase()}`

---

### UX сообщения

```js
messages: {
  wait: "🜁 Генерируем…",
  processing: "⏳ Подожди, ещё формируется…",
  error: "⚠️ Не удалось получить изображение"
}
```

* `wait` — показываем при отправке (PHASE 1)
* `processing` — показываем если пользователь пишет во время ожидания (PHASE 2, но это не callback)
* `error` — сообщение по умолчанию при ошибке (если нет `error.script`)

---

### Ошибки

```js
error: {
  script: "Orion_Image_Error",
  flagAttr: "orion_image_error",
  reasonAttr: "orion_image_error_reason"
}
```

* `flagAttr` / `reasonAttr` — сохранить состояние ошибки в lead
* `script` — куда перейти при ошибке (опционально)

---

### Успешный выход

```js
successScript: "Orion_Image_Ready"
```

Если не указан — `ImageGen.run()` возвращается в ту же точку (`return true`), без перехода.

---

## Таймаут: встроенный vs внешний

В большинстве сценариев таймаут проще и чище задавать **внутри** `ImageGen` через `timeout`, потому что плагин сам использует `Common.Platform.AsyncFallback`.

Внешнее управление таймаутом имеет смысл, если:

* вы хотите один общий timeout на несколько async-операций
* вы централизованно оркестрируете несколько вызовов с одним namespace/policy

---

## Примеры использования

### Пример 1 — как используется в Orion: system prompt из таблицы + timeout + error + URL (MVP)

```js
/**
 * orion_profiling_reflection_image
 *
 * Назначение:
 * - асинхронно сгенерировать вертикальный "Operator Reflection" образ
 * - сохранить URL в лид (скачивание/сохранение — отдельным шагом)
 * - timeout + error внутри ImageGen (как у LLMQuery)
 */

const ImageGen = require("Common.AI.ImageGen");

return ImageGen.run({
  lead,
  isFirstImmediateCall,

  code: "OrionActorImage",

  // Provider/Auth
  provider: "openai",
  auth: { tokenKey: "OPENAI_API_KEY" },

  // Prompts из таблицы + inline
  agentName: "orion",
  promptTable: "gpt_prompts",
  prompts: {
    // системный слой (табличный)
    system: ["$avatar_brief_generator"],

    // основной запрос (inline)
    user: [`
Create a vertical codex-grade mythotech Operator icon in Aurum Void aesthetic.

Aurum Void: cold matte gold schematic lines (axes, nodes, ritual UI glyphs) over deep graphite void.
No glossy sci-fi, no neon, no superhero vibe, no humor.

Faceless figure (hood/shadow/mask/void), calm, stable, centered on a strong vertical axis.
Heavy materials: carbon composite armor/robe, matte metal, dense fabric, worn realistic textures.
Subtle fog/particles for depth.

This is NOT a human portrait. It is an operational manifestation of a system entrepreneur / product leader at the scaling stage.
Output: a visual artifact, not an illustration.
    `.trim()]
  },

  // Таймаут (fallback)
  timeout: {
    seconds: 120,
    script: "Orion_Image_Timeout"
  },

  // Ошибки (как в LLMQuery)
  error: {
    script: "Orion_Image_Error",
    flagAttr: "orion_image_error",
    reasonAttr: "orion_image_error_reason"
  },

  // Namespace чтобы не конфликтовать
  fallback: {
    namespace: "orion_image_reflection"
  },

  // MVP: хотим именно URL
  requireUrl: true,

  image: {
    model: "dall-e-3",
    size: "1024x1792",
    quality: "standard",
    style: "natural",
    response_format: "url"
  },

  messages: {
    wait: "🜁 ORION формирует визуальное отражение…",
    processing: "⏳ Подожди, образ ещё куется…",
    error: "⚠️ Не удалось получить изображение"
  },

  save: {
    urlAttr: "orion_actor_image_url",
    b64Attr: "orion_actor_image_b64",
    rawJsonAttr: "orion_actor_image_payload"
  }

  // successScript: "Orion_Image_Ready" // опционально
});
```

---

### Пример 2 — минимальный сценарий с таблицей промптов, без successScript

Подходит, когда вы хотите вернуться “в ту же точку” и решать дальше в текущем шаге.

```js
const ImageGen = require("Common.AI.ImageGen");

return ImageGen.run({
  lead,
  isFirstImmediateCall,

  code: "OperatorIcon",

  provider: "openai",
  auth: { tokenKey: "OPENAI_API_KEY" },

  agentName: "orion",
  promptTable: "gpt_prompts",
  prompts: {
    system: ["$avatar_brief_generator"],
    user: ["Create a vertical mythotech Operator icon in Aurum Void aesthetic..."]
  },

  timeout: { seconds: 90, script: "Image_Timeout" },
  error: { script: "Image_Error" },

  requireUrl: true,

  image: {
    model: "dall-e-3",
    size: "1024x1792",
    response_format: "url"
  },

  save: {
    urlAttr: "operator_icon_url",
    rawJsonAttr: "operator_icon_payload"
  }
});
```

---

### Пример 3 — внешний контроль timeout через AsyncFallback (продвинутый режим)

Этот способ полезен, если:

* у вас одна общая политика таймаутов
* вы управляете фоллбеками централизованно (например, несколько разных async-операций в одном шаге)

```js
const ImageGen = require("Common.AI.ImageGen");
const AsyncFallback = require("Common.Platform.AsyncFallback");

if (isFirstImmediateCall) {
  AsyncFallback.configure({
    lead,
    namespace: "orion_image_reflection",
    timeout: { seconds: 120, script: "Orion_Image_Timeout" },
    error: { flagAttr: "orion_image_error", reasonAttr: "orion_image_error_reason" }
  }).schedule();
}

const res = ImageGen.run({
  lead,
  isFirstImmediateCall,

  code: "OrionActorImage",
  provider: "openai",
  auth: { tokenKey: "OPENAI_API_KEY" },

  agentName: "orion",
  promptTable: "gpt_prompts",
  prompts: {
    system: ["$avatar_brief_generator"],
    user: ["Create a vertical mythotech Operator icon in Aurum Void aesthetic..."]
  },

  // timeout внутри не задаём, потому что контролируем снаружи
  error: {
    script: "Orion_Image_Error",
    flagAttr: "orion_image_error",
    reasonAttr: "orion_image_error_reason"
  },

  requireUrl: true,

  image: {
    model: "dall-e-3",
    size: "1024x1792",
    response_format: "url"
  },

  save: {
    urlAttr: "orion_actor_image_url",
    rawJsonAttr: "orion_actor_image_payload"
  }
});

if (!isFirstImmediateCall && res === true) {
  AsyncFallback.configure({
    lead,
    namespace: "orion_image_reflection"
  }).unschedule();
}

return res;
```

---

## Частые сценарии отказов и как реагировать

* **send_failed** — процессор не принял запрос / RemoteApiCall не отработал
  → `error.script` или retry

* **provider_error** — провайдер вернул ошибку (например, HTTP != 200)
  → смотреть `save.rawJsonAttr`, логировать, предлагать retry

* **url_missing** при `requireUrl=true`
  → либо переключиться на `b64_json`, либо поменять модель/response_format

* **no_result** — ответ пришёл, но данных нет
  → сохранить `rawJsonAttr` и смотреть, что вернул провайдер/процессор

* **timeout** (через `timeout.script`)
  → отдельный timeout-script может предложить повторить генерацию или вернуться в меню

---

## Итог

`Common.AI.ImageGen` — стандартизированный способ подключить генерацию изображений в сценарии Metabot:

* двухфазный async flow
* сохранение результата в lead
* UX на случай “пользователь пишет во время ожидания”
* встроенный timeout через `Common.Platform.AsyncFallback`
* работа с промптами как с массивами, включая табличные `$alias` через `Common.AI.Prompts`
* расширяемая система провайдеров (сейчас OpenAI)

# Common.AI.Prompts — Универсальный резолвер и сборщик промптов

**Автор:** Art Yg  
**Версия:** 1.0

`Prompts` — инфраструктурный helper для **унифицированной работы с промптами** в сценариях Metabot и внутри других плагинов (например, `Common.AI.ImageGen`, `LLMQuery/LMClient`).

Он решает типовую боль: **не держать промпты в коде**, не копировать одно и то же “склеивание”, и иметь единый механизм:

* подтянуть промпт из таблицы по ссылке (`$name`, `@name`)
* применить макросы на основе `lead` и `bot`
* нормализовать вход (строка/массив/что угодно)
* собрать итоговый текст промпта из блоков (`system/user/last`)

---

## Зачем он существует

В продуктовых сценариях промпты обычно:

* растут и ветвятся по агентам (`agentName`)
* переезжают в таблицы/консоль управления (а не в JS)
* используют переменные окружения (lead/bot attrs)
* собираются из нескольких частей (“системный каркас” + “задача” + “контекст”)

Без `Prompts` это превращается в дублирование:

* вручную ходим в `table.find(...)`
* вручную проверяем, что `$alias` нашли
* вручную подставляем `lead.getAttr(...)`
* вручную склеиваем массивы

`Prompts` стандартизирует это как **маленький инфраструктурный слой**, который можно подключать где угодно.

---

## Основные принципы

* **Lead-first** — `lead` передаётся явно (без неявной магии).
* **Bot runtime** — `bot` берётся из глобального окружения runtime.
* **Refs only if asked** — в таблицу лезем **только если** ты используешь `$...` или `@...`.
* **Strict by default** — если промпт по ссылке не найден → ошибка (чтобы не генерить “пустоту” молча).
* **Composable** — подходит и как отдельный helper, и как зависимость внутри других плагинов.

---

## Минимальные требования

Для работы в “inline-режиме” достаточно:

1. `lead`
2. вызвать `Prompts.buildText(...)`

Для работы с табличными ссылками (`$...` / `@...`) дополнительно нужно:

1. наличие `table.find` в runtime
2. таблица промптов (`promptTable`)
3. агент (`agentName`) — **обязателен только для `$name`**

---

## Что умеет Prompts

### 1) Ссылки на промпты из таблицы

Поддерживаются два типа ref:

* `$name` — промпт из таблицы для **конкретного агента**
* `@name` — промпт из таблицы для **общего агента `<<common>>`**

Пример:

* `$avatar_brief_generator` → `agentName = "orion"`
* `@safety_rules` → `agentName` не нужен (используется `<<common>>`)

---

### 2) Макросы (переменные из lead и bot)

`Prompts` умеет подставлять значения из атрибутов:

* `{{$key}}` → из `lead` (attr или json)
* `{{$$key}}` → из `bot` (attr или json)

Поведение:

* если найден json-атрибут → подставляет `JSON.stringify(value)`
* иначе подставляет строковый `getAttr`
* если не найдено → подставляет пустую строку

---

### 3) Нормализация входов и сборка блоков

На вход можно дать:

* `null/undefined` → станет `[]`
* `string` → станет `[string]`
* `array` → останется array
* `object/number` → станет `[String(value)]`

Дальше `Prompts` собирает:

* `system[]`
* `user[]`
* `last[]`
* `all[]` = `system + user + last`

И может вернуть:

* либо blocks (`buildBlocks`)
* либо финальную строку (`buildText`) через `join("\n\n")`

---

## Конфигурация

Все методы используют общие опции.

### DEFAULTS

```js
{
  promptTable: "gpt_prompts",
  agentName: null,   // обязателен для "$name"
  strict: true,      // если промпт не найден → throw
  applyMacros: true
}
```

### Важные правила

* `agentName` **обязателен только** если ты используешь `$name`
* `promptTable` обязателен для `$name` и `@name`
* если нет `$`/`@` refs — можно вообще не передавать `agentName/promptTable`

---

## API Prompts

### Prompts.toArray(value)

Нормализует значение в массив строк.

Используется внутри, но можно использовать и снаружи.

---

### Prompts.resolveOne(ref, opts)

Резолвит одну строку:

* `$name` → ищет в `promptTable` по `agentName`
* `@name` → ищет в `promptTable` по агенту `<<common>>`
* обычная строка → возвращается как есть

---

### Prompts.resolveMany(list, opts)

Резолвит список refs/строк → массив строк.

---

### Prompts.applyMacros(str, lead)

Применяет:

* `{{$key}}` из lead
* `{{$$key}}` из bot

---

### Prompts.buildBlocks(input, opts)

Собирает блоки по секциям:

```js
{
  system: [],
  user: [],
  last: [],
  all: []
}
```

---

### Prompts.buildText(input, opts)

Собирает итоговый prompt как строку:

* вызывает `buildBlocks`
* склеивает `all.join("\n\n")`

---

## Табличный формат промптов

`Prompts` ожидает, что таблица (`promptTable`) содержит хотя бы поля:

* `agent_name`
* `name`
* `prompt`

Запрос выполняется через:

```js
table.find(promptTable, ["prompt"], [
  ["agent_name", agent],
  ["name", name]
]);
```

---

## Использование

Ниже примеры именно “для статьи”: чтобы читатель увидел, что есть несколько режимов и зачем это.

---

## Примеры

### Пример 1 — Inline: без таблиц, без агента

Подходит для простых сценариев и MVP.

```js
const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: [
      "You are a strict image generator. Output must be cinematic, realistic, and calm."
    ],
    user: [
      "Create a vertical mythotech Operator icon in Aurum Void aesthetic."
    ]
  },
  { lead } // agentName/promptTable не нужны
);

// prompt — готовая строка, без обращений к таблицам
```

---

### Пример 2 — Табличный промпт агента: `$alias`

Если используешь `$...`, то **agentName обязателен**, иначе будет throw.

```js
const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: [
      "$avatar_brief_generator" // берём из таблицы для агента orion
    ],
    user: [
      "Output should be a codex-grade artifact. No neon. No superhero vibe."
    ]
  },
  {
    lead,
    agentName: "orion",
    promptTable: "gpt_prompts"
  }
);
```

---

### Пример 3 — Общий промпт: `@alias` (agentName не нужен)

`@name` всегда читается из агента `<<common>>`.

```js
const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: [
      "@safety_rules",
      "@style_aurum_void"
    ],
    user: [
      "Create an abstract Operator icon."
    ]
  },
  {
    lead,
    promptTable: "gpt_prompts"
  }
);
```

---

### Пример 4 — Макросы: подтягиваем контекст из lead и bot

```js
const Prompts = require("Common.AI.Prompts");

// допустим:
// lead.getAttr("actor_stage") = "scaling"
// bot.getAttr("BRAND_TONE") = "discipline, meaning, depth"

const prompt = Prompts.buildText(
  {
    system: [
      "Tone: {{$$BRAND_TONE}}"
    ],
    user: [
      "Stage: {{$actor_stage}}",
      "Create a vertical Operator artifact."
    ]
  },
  {
    lead,
    applyMacros: true
  }
);
```

---

### Пример 5 — Сборка blocks отдельно (для отладки/логирования)

Иногда полезно видеть, какие блоки получились до склейки.

```js
const Prompts = require("Common.AI.Prompts");

const blocks = Prompts.buildBlocks(
  {
    system: ["@style_aurum_void", "$avatar_brief_generator"],
    user: ["Generate an icon for current stage: {{$actor_stage}}"]
  },
  {
    lead,
    agentName: "orion",
    promptTable: "gpt_prompts"
  }
);

// blocks.system / blocks.user / blocks.all — можно сохранить в lead или tracer
```

---

### Пример 6 — Мягкий режим (strict=false)

Иногда нужен режим “не падать”, а вернуть сообщение/заглушку.

```js
const Prompts = require("Common.AI.Prompts");

const prompt = Prompts.buildText(
  {
    system: ["$missing_alias"],
    user: ["Create an icon."]
  },
  {
    lead,
    agentName: "orion",
    promptTable: "gpt_prompts",
    strict: false
  }
);

// В strict=false при отсутствии промпта вернётся текст-предупреждение (а не throw)
```

---

## Использование внутри других плагинов

`Common.AI.Prompts` специально сделан как “маленький кусок инфраструктуры”, чтобы:

* не тащить целиком LMClient ради таблиц промптов
* не повторять код резолва в каждом AI-плагине
* унифицировать поведение `$alias` / `@alias` и макросов

### Где он уже естественно применяется

* `Common.AI.ImageGen` — чтобы собирать итоговый `image.prompt` из блоков и таблиц
* потенциально любой “AI transport plugin”: STT, TTS, embeddings, classification, etc.

---

## Частые ошибки и как их избежать

### 1) Использовали `$alias`, но не указали agentName

Это **ошибка по контракту**, будет throw:

* потому что `$alias` — агентный промпт
* без `agentName` невозможно определить неймспейс

Решение: передай `agentName`.

---

### 2) Использовали `$alias` / `@alias`, но не указали promptTable

Это тоже ошибка:

* потому что непонятно, откуда искать

Решение: передай `promptTable` (или оставь дефолт `"gpt_prompts"`).

---

### 3) Хотели “просто текст”, но случайно начали строку с `$`

Если текст реально должен начинаться с `$`, то сейчас это будет воспринято как ref.

Практический паттерн: не начинай “сырой текст” с `$`/`@`.
Если прям надо — лучше добавить пробел или явную экранировку на уровне твоего контента.

---

## Итог

`Common.AI.Prompts` — это базовый инфраструктурный helper, который:

* даёт единый формат сборки промптов (`system/user/last`)
* вытаскивает промпты из таблиц (`$agent`, `@common`)
* подставляет переменные из lead/bot
* снижает дублирование и делает AI-плагины проще и чище

Он может использоваться:

* **как самостоятельный utility** в сценариях
* **как зависимость** внутри более крупных AI-плагинов (например, `ImageGen`)

Если в твоей архитектуре дальше появятся новые источники промптов (реестр провайдеров, JSON-конфиги, версии промптов, AB-тесты) — вот этот слой и будет правильным местом для расширения. И это как раз тот случай, где можно внезапно сделать себе ловушку, если начать “подмешивать” сюда бизнес-логику — держи его инфраструктурным, иначе потом будет боль.

# Компонентная разработка vs «Durex-код»

##### 📘 Памятка инженера Metabot

# 1️⃣ В чём разница подходов

<table id="bkmrk-%D0%A1%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D0%B2%D1%8B%D0%B9-%D0%BF%D0%BE%D0%B4%D1%85%D0%BE%D0%B4-%D0%9A%D0%BE"><thead><tr><th>Скриптовый подход</th><th>Компонентный подход</th></tr></thead><tbody><tr><td>Код вставляется прямо в сценарий</td><td>Сценарий использует готовый компонент</td></tr><tr><td>Telegram-логика торчит в webhook-скрипте</td><td>Telegram спрятан внутри плагина</td></tr><tr><td>Сценарист не может этим пользоваться</td><td>Сценарист работает декларативно</td></tr><tr><td>Нет чёткого контракта</td><td>Есть входы, выходы, параметры</td></tr><tr><td>Разовое решение</td><td>Переиспользуемый модуль</td></tr><tr><td>Зависит от конкретного проекта</td><td>Доставляется в любой проект</td></tr></tbody></table>

---

# 2️⃣ Что такое «Durex-код»

Durex-код — это:

- код, написанный «под задачу»
- код, который нельзя переиспользовать
- код, который живёт только в одном скрипте
- код, который нарушает контракты
- код, который смешивает логику канала, бизнес-логику и UX

Это не плохо. Иногда это допустимо.

Но это **не архитектурный стиль Metabot**.

---

# 3️⃣ Что такое компонентный стиль Metabot

Компонент — это:

- атомарная команда
- с чёткими входами
- с чёткими выходами
- с понятным поведением
- с изолированной зоной ответственности
- со спрятанной инженерной сложностью

Пример:

```
const VoiceInput = require('Common.Voice.VoiceInput')

return VoiceInput.expect({
  code: "orion_profiling_q1_voice",

  lead,

  successScript: "orion_profiling_q2",
  cancelScript: "orion_profiling_cancelled",

  targetAttr: "orion_profiling_q1_text",
  sourceAttr: "orion_profiling_q1_voice_url",

  extraAttrs: {
    active_agent: "orion",
    voice_context: "orion_profiling_q1",
    input_mode: "profiling"
  },

  processorScript: "System_VoiceInput_Processor",

  stt: {
    provider: "openai",
    options: { model: "whisper-1", language: "ru" },
    asyncResponse: true,
    tokenKey: "OPENAI_API_KEY"
  }
})

```

Сценарист видит:

- targetAttr
- sourceAttr
- successScript
- cancelScript
- stt-параметры

Он не видит:

- Telegram API
- file\_id
- webhook структуру
- async механику
- OpenAI SDK

Это декларативный стиль.

---

# 4️⃣ Что такое декларативный подход

Декларативный подход — это:

> Мы описываем ЧТО должно произойти, а не КАК это происходит.

Сценарист пишет:

```
Ожидай голос.
Перейди в success.
Сохрани результат.

```

А не:

```
Проверь payload.message.voice
Достань file_id
Сходи в Telegram API
Отправь в STT
Подожди callback
Распарси JSON

```

---

# 5️⃣ Принципы компонентной разработки Metabot

## 1. Чёткий контракт

Каждый компонент обязан иметь:

- входные параметры
- выходные данные
- предсказуемое поведение
- задокументированные переходы

---

## 2. Нет неявной передачи данных

❌ Плохо:

```js
memory.foo = 123

```

Потом в другом месте:

```js
memory.foo

```

Это разрыв контракта.

✔ Правильно:

- вход через параметры
- управляемый выход, например, через targetAttr
- всё прозрачно

---

## 3. Одна зона ответственности

Пример:

VoiceInput:

- отвечает за голосовой ввод
- не отвечает за каналы
- не отвечает за Telegram

ArtifactResolver:

- отвечает за извлечение артефактов из всех каналов
- не отвечает за STT
- не отвечает за сценарий

---

## 4. Никаких «всё в один файл»

Если логика растёт — создаём слой.

Пример:

- Был Telegram-код внутри VoiceInput
- Появился Max
- Вместо if/else-ада — создаём Channel.ArtifactResolver

Это инженерный подход.

---

## 5. Повторное использование — обязательное требование

Если модуль нельзя переиспользовать — это не компонент.

Компонент должен быть:

- переносимым
- подключаемым
- поставляемым как плагин

---

# 6️⃣ Почему это важно

1. Сценаристам проще
2. Интеграторам проще
3. Код чище
4. Проекты масштабируются
5. Плагины можно доставлять в разные компании
6. Мы получаем архитектурную степень свободы

И самое интересное:

⏱ По времени это занимает примерно столько же.

Разница — в мышлении.

---

## 6.1 🔓 Декларативный стиль = расширяемая палитра Metabot

Декларативный подход — это не только «красиво».

Это позволяет нам:

- расширять палитру компонентов
- развивать low-code платформу
- строить визуальные конструкторы
- делать архитектуру масштабируемой

Сейчас в Metabot есть ~21 атомарная команда.

Примеры:

- SendMessage
- SendImage
- SendFile
- Input
- VoiceInput
- TransferToOperator
- RunScript
- и т.д.

Каждый компонент:

- имеет входы
- имеет выходы
- имеет чёткие параметры
- ведёт себя предсказуемо

Это позволяет:

1️⃣ Легко расширять платформу Добавили новый компонент → он сразу становится частью палитры.

2️⃣ Делать визуальные конструкторы Если у компонента есть вход/выход — можно рисовать схемы.

3️⃣ Строить workflow-системы Компоненты становятся узлами графа.

4️⃣ Делать экспортируемые решения Компонент можно доставить в другой проект.

---

## 6.2 🧱 Атомарность = возможность визуализации

Если компонент описан как:

- чёрный ящик
- с чёткими входами
- с чёткими выходами

его можно:

- визуализировать
- соединять стрелками
- использовать в low-code
- использовать в no-code
- переносить между проектами

Если код:

- размазан по скриптам
- читает memory
- меняет глобальные объекты
- зависит от webhook структуры

его нельзя:

- визуализировать
- стандартизировать
- масштабировать

---

# 7️⃣ Архитектор vs Скриптовик

Скриптовик думает:

> Мне нужно сделать, чтобы работало.

Архитектор думает:

> Как сделать, чтобы это работало в 10 проектах.

---

# 8️⃣ Как использовать это как AI-чеклист

Можно кидать в AI вместе с кодом и спрашивать перед коммитом:

- Есть ли здесь чёткий контракт?
- Понятен ли этот код сценаристу?
- Есть ли неявная передача данных?
- Нарушена ли зона ответственности?
- Можно ли переиспользовать этот код?
- Можно ли этот код вынести в компонент?
- Можно ли визуализировать это поведение как узел workflow?
- Является ли это Durex-кодом или системным модулем?

Если на 3+ вопроса ответ «нет» — ты пишешь Durex.

---

# 9️⃣ Когда допустим Durex-код

Иногда допустимо:

- разовая миграция
- временный костыль
- hotfix

Но:

- он не должен становиться системой
- он не должен множиться
- он не должен ломать архитектуру

---

# 1️⃣0️⃣ Когда допустимо писать код в скрипте

Код в скрипте допускается.

Но только если соблюдены условия.

---

## ✔ Допустимые случаи

### 1️⃣ Абсолютно разовая логика

- временная миграция
- одноразовый расчёт
- локальный pipeline
- подсчёт баллов
- простая условная логика

Пример:

```js
if (lead.getAttr("score") > 10) {
  return bot.run({ script_code: "vip_branch" })
}

```

Это допустимо.

---

### 2️⃣ Логика понятна сценаристу

Если сценарист:

- может прочитать
- может понять
- может отредактировать

— код допустим.

Если сценарист не может понять код — код не должен находиться в сценарии.

Исключение — системные вещи без которых абсолютно нельзя обойтись, например, вызов плагинов.

---

### 3️⃣ Логика не нарушает архитектуру

Допустимый код:

- не читает неявные memory
- не зависит от глобальных переменных
- не создаёт скрытых зависимостей
- не нарушает контракт компонентов

---

## ❌ Недопустимый Durex-код

Код нельзя писать в сценарии, если:

- он сложный
- он интеграционный
- он работает с API
- он обрабатывает webhook
- он содержит асинхронную логику
- он требует инженерного уровня знаний

Такой код должен быть:

- вынесен в плагин
- спрятан под компонент
- задокументирован
- переиспользуем

---

# 1️⃣1️⃣ Финальная мысль

Metabot — это не набор скриптов.

Это:

- палитра компонентов
- декларативный конструктор
- платформа

Если ты пишешь код, который нельзя вынести в палитру — ты... выпускаешь Durex.

Если ты пишешь код, который расширяет палитру — ты работаешь как инженер платформы.

**P.S. Почему «Durex»?**

Компонент — это **капитал**: инвестиция и актив, который ставится на баланс. Его можно переиспользовать, масштабировать и доставлять в десятках проектов — он создаёт системную ценность.

А разовый скрипт — это **расход периода**: проект сделали, прибыль получили — но актива не появилось. Работу сделали один раз, в следующем проекте — делаем то же самое заново. Все при деле, задача выполняется, но капитал не создаётся.

# HTTP



# Common.HTTP.ProxyFetch — загрузка URL-контента через прокси

**Пакет:** `Http`  
**Полное имя компонента:** `Common.Http.ProxyFetch`

## Что это

**ProxyFetch** — системный PHP-компонент (V8Wrapper) для загрузки содержимого по HTTP/HTTPS URL внутри сценариев Metabot.

Проще всего воспринимать так:

**это аналог `file_get_contents($url)`, но с поддержкой прокси, таймаутов и повторных попыток**, на том же cURL-стеке, что используется для надёжных загрузок в платформе (`CdnFtp`).

## Зачем нужен ProxyFetch

- `file_get_contents()` и часть HTTP-клиентов **не используют** прокси из окружения инстанса → запросы «уходят мимо» SOCKS5/HTTP-прокси и падают или «висят».
- Нет единых **таймаутов** и **ретраев** → сложнее переживать кратковременные сбои CDN и 5xx.
- Дублировать логику прокси в каждом плагине нецелесообразно.

ProxyFetch даёт **одну точку входа** для сценариев и плагинов: загрузить строку по URL или получить метаданные (HEAD) с теми же правилами отказоустойчивости.

## Два контракта: generic fetch vs файловая загрузка

В платформе **два разных метода** с **разной семантикой успеха**. Не путайте их:

<table id="bkmrk-fetchurlcontents%28%29-%2F"><thead><tr><th>  
</th><th>`fetchUrlContents()` / ProxyFetch</th><th>`downloadFileFromUrlToBusiness()`</th></tr></thead><tbody><tr><td>**Назначение**</td><td>Получить тело ответа в память</td><td>Скачать и сохранить файл на диск</td></tr><tr><td>**`ok` / успех**</td><td>`curl_exec !== false` и `http_code > 0`</td><td>Только `2xx + non-empty body`</td></tr><tr><td>**404**</td><td>`ok=true`, `http_code=404` — **by design**</td><td>Ошибка, return `null`</td></tr><tr><td>**Пустой body**</td><td>Допустим (204, HEAD-like API)</td><td>Ошибка</td></tr><tr><td>**Retry**</td><td>cURL error / `http_code=0` / `5xx`</td><td>cURL error / `http_code=0` / не-2xx / пустой body</td></tr></tbody></table>

> **Важно:** если кто-то «оптимизирует» один контракт под другой — сломает либо плагины, либо загрузку файлов.

## Где используется

- Загрузка CSV/TXT/JSON по URL перед разбором в сценарии.
- Предварительная проверка размера/доступности файла по URL.
- Замена прямых `file_get_contents` в плагинах вроде `Common.Files.Converter`, `Common.Integrations.Request` (миграция по желанию команды).
- Любые кейсы, где бот на сервере обязан ходить в интернет **только через прокси**.

## Где находится компонент

Подключение в JavaScript-сценарии (V8):

```javascript
const ProxyFetch = require("Common.Http.ProxyFetch")

```

PHP-класс плагина:

`Plugins\Dynamic\Common\Http\V8Wrapper\ProxyFetch`

Платформенное ядро (можно вызывать из PHP-плагинов без V8):

`App\Services\CdnFtp::fetchUrlContents()`  
`App\Services\CdnFtp::getFileInfoByUrl()` (расширенная сигнатура с прокси и ретраями)

## Как работает ProxyFetch

Архитектура из трёх слоёв:

```text
V8 JS  →  Common.Http.ProxyFetch  →  mergeWithDefaults (http_outbound + options)
       →  CdnFtp::fetchUrlContents / getFileInfoByUrl
       →  CdnFtp::curlExecWithFallback / curlExec  (общий cURL-раннер)
       →  cURL (прокси, таймауты, SSL, exec, диагностика)

```

**Общий раннер:** `curlExec()` — единая точка init/proxy/timeout/SSL/exec/close; `curlExecWithFallback()` — обёртка с `href_encode(url)` → fallback на raw URL. Переиспользуется в `fetchUrlContents` и `getFileInfoByUrl`.

**Ретраи:** отдельные «раунды»; в каждом раунде `curlExecWithFallback` перебирает варианты URL. Повтор, если `curl_exec` дал сбой, `http_code === 0` или `http_code >= 500`.

**Логирование:** при `HTTP_OUTBOUND_LOG_REQUESTS=true`:

- `CdnFtp outbound fetch: retry ...` / `failed after ...` — для fetchUrlContents
- `CdnFtp outbound fileInfo: retry ...` / `failed after ...` — для getFileInfoByUrl

## Что нужно настроить

### 1. Конфиг `config/http_outbound.php`

Загружается автоматически; значения берутся из `.env`.

### 2. Переменные `.env`

<table id="bkmrk-%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F-%D0%9D%D0%B0%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8"><thead><tr><th>Переменная</th><th>Назначение</th></tr></thead><tbody><tr><td>`HTTP_OUTBOUND_PROXY`</td><td>Строка прокси (например `socks5://login:password@host:port`). Пусто — прокси не задаётся.</td></tr><tr><td>`HTTP_OUTBOUND_PROXY_VERSION`</td><td>Опционально: `v4` или `v6` (разрешение DNS/IP для cURL).</td></tr><tr><td>`HTTP_OUTBOUND_TIMEOUT`</td><td>Таймаут запроса, сек (по умолчанию 30).</td></tr><tr><td>`HTTP_OUTBOUND_CONNECT_TIMEOUT`</td><td>Таймаут на установку соединения, сек (по умолчанию 10).</td></tr><tr><td>`HTTP_OUTBOUND_RETRIES`</td><td>Число раундов попыток (по умолчанию 3).</td></tr><tr><td>`HTTP_OUTBOUND_RETRY_AFTER_SEC`</td><td>Пауза между раундами, сек (по умолчанию 2).</td></tr><tr><td>`HTTP_OUTBOUND_LOG_REQUESTS`</td><td>`true`/`false` — писать диагностические строки в лог (по умолчанию true).</td></tr></tbody></table>

**Важно:** эта группа **независима** от `botman.telegram`. Если на проекте один прокси на всё, можно задать одинаковые значения в обеих группах; если прокси разные — настраивайте раздельно.

### 3. Установка плагина на инстансе

Файл уже в `Plugins/Dynamic/Common/Http/V8Wrapper/ProxyFetch.php`. Автозагрузка через SPL autoload в `ModuleLoader` — дополнительных действий не требуется.

## Создание плагина в админке

Если плагин **не** поставляется из репозитория, создайте его вручную:

1. **Плагин:** имя каталога/пакета `Http`, тип стандартный, **Common**, уровень доступа **SYSTEM**.
2. **Скрипт:** имя `ProxyFetch`, тип **PHP (WRAPPER FOR V8)**.
3. Вставьте код из файла `Plugins/Dynamic/Common/Http/V8Wrapper/ProxyFetch.php` (namespace и имя класса должны совпадать).
4. Включите скрипт (**is\_enabled**).

Платформа сохранит PHP-файл в структуру динамических плагинов; `ModuleLoader` через SPL autoload видит класс автоматически.

## Сигнатура вызова (JavaScript / V8)

### `getContents`

```javascript
const ProxyFetch = require("Common.Http.ProxyFetch")

let result = ProxyFetch.getContents("https://example.com/data.csv")

if (!result.ok) {
  debug("Ошибка загрузки: " + result.last_curl_error)
  return false
}

let text = result.content

```

С переопределением параметров:

```javascript
let result = ProxyFetch.getContents("https://example.com/big.bin", {
  proxy: "socks5://user:pass@proxy.example:1080",
  timeout: 120,
  connect_timeout: 15,
  max_attempts: 5,
  retry_after_sec: 3
})

```

### `getFileInfo`

```javascript
let info = ProxyFetch.getFileInfo("https://example.com/photo.jpg")

if (info.exists && info.size_kb < 20480) {
  // условно безопасный размер
}

```

## Параметры `options`

<table id="bkmrk-%D0%9F%D0%BE%D0%BB%D0%B5-%D0%A2%D0%B8%D0%BF-%D0%9E%D0%B1%D1%8F%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D0%B5%D0%BD-"><thead><tr><th>Поле</th><th>Тип</th><th>Обязателен</th><th>Описание</th></tr></thead><tbody><tr><td>`proxy`</td><td>string</td><td>Нет</td><td>Переопределить прокси для этого вызова</td></tr><tr><td>`timeout`</td><td>number</td><td>Нет</td><td>Таймаут cURL, сек (дефолт из `http_outbound.timeout`)</td></tr><tr><td>`connect_timeout`</td><td>number</td><td>Нет</td><td>Таймаут соединения, сек</td></tr><tr><td>`max_attempts`</td><td>number</td><td>Нет</td><td>Число раундов ретраев (`http_outbound.retries`)</td></tr><tr><td>`retry_after_sec`</td><td>number</td><td>Нет</td><td>Пауза между раундами</td></tr><tr><td>`log_requests`</td><td>boolean</td><td>Нет</td><td>Логировать попытки (`http_outbound.log_requests`)</td></tr></tbody></table>

## Формат ответа `getContents`

<table id="bkmrk-%D0%9F%D0%BE%D0%BB%D0%B5-%D0%A2%D0%B8%D0%BF-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-ok"><thead><tr><th>Поле</th><th>Тип</th><th>Описание</th></tr></thead><tbody><tr><td>`ok`</td><td>boolean</td><td>Транспорт доставил ответ: `curl_exec !== false` и `http_code > 0`</td></tr><tr><td>`content`</td><td>string | null</td><td>Тело ответа; `null` при транспортном провале</td></tr><tr><td>`http_code`</td><td>number</td><td>HTTP-код последнего ответа</td></tr><tr><td>`attempts`</td><td>number</td><td>Сколько раундов было выполнено</td></tr><tr><td>`last_curl_error`</td><td>string</td><td>Текст ошибки при `ok === false`; при успехе — пустая строка</td></tr></tbody></table>

**Про 404:** при ответе 404 платформа вернёт `ok === true` и тело страницы ошибки — **всегда проверяйте `http_code`**, если вам нужен именно успешный ресурс. Это by design: generic fetch не навязывает семантику HTTP-статусов.

## Формат ответа `getFileInfo`

Прежние поля:

- `exists` (0/1) — по сути «код ответа 200».
- `size_kb`, `type`, `mime_type`, `name`.

Добавлены:

- `http_code`
- `attempts`
- `last_curl_error`

## Использование из PHP (без V8)

```php
use App\Services\CdnFtp;

$result = CdnFtp::fetchUrlContents(
    $url,
    config('http_outbound.proxy'),
    (int) config('http_outbound.timeout', 30),
    (int) config('http_outbound.connect_timeout', 10),
    (int) config('http_outbound.retries', 3),
    (int) config('http_outbound.retry_after_sec', 2),
    (bool) config('http_outbound.log_requests', true)
);

if (!$result['ok']) {
    // логировать $result['last_curl_error']
}

```

HEAD / метаданные:

```php
$info = CdnFtp::getFileInfoByUrl(
    $url,
    config('http_outbound.proxy'),
    (int) config('http_outbound.connect_timeout', 10),
    (int) config('http_outbound.timeout', 30),
    (int) config('http_outbound.retries', 3),
    (int) config('http_outbound.retry_after_sec', 2)
);

```

Одноаргументный вызов `CdnFtp::getFileInfoByUrl($url)` сохраняет старое поведение (без таймаутов из `http_outbound`).

## Пример замены `file_get_contents` в плагине

**Было:**

```php
$data = file_get_contents($url);

```

**Стало (через ядро):**

```php
$fetch = \App\Services\CdnFtp::fetchUrlContents(
    $url,
    config('http_outbound.proxy'),
    (int) config('http_outbound.timeout', 30),
    (int) config('http_outbound.connect_timeout', 10),
    (int) config('http_outbound.retries', 3),
    (int) config('http_outbound.retry_after_sec', 2)
);
if (!$fetch['ok']) {
    throw new \RuntimeException($fetch['last_curl_error'] ?: 'fetch failed');
}
$data = $fetch['content'];

```

## Обработка ошибок

<table id="bkmrk-%D0%A1%D0%B8%D1%82%D1%83%D0%B0%D1%86%D0%B8%D1%8F-%D0%A7%D1%82%D0%BE-%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D1%82%D1%8C"><thead><tr><th>Ситуация</th><th>Что ожидать</th></tr></thead><tbody><tr><td>Таймаут</td><td>`ok === false`, в `last_curl_error` часто `cURL error 28: ...`</td></tr><tr><td>Прокси недоступен</td><td>`ok === false`, типично `cURL error 7: ...`</td></tr><tr><td>5xx после ретраев</td><td>`ok === false`, `last_curl_error` содержит `HTTP 503` и т.д.</td></tr><tr><td>404</td><td>`ok === true`, `http_code === 404` — проверяйте код</td></tr><tr><td>Невалидный URL / пустая строка</td><td>`ok === false`, `last_curl_error` поясняет причину</td></tr></tbody></table>

Сетевую ошибку и «битый» контент (HTML вместо CSV) различайте по **`http_code`** и валидации содержимого в сценарии.

## Как отлаживать

1. **`.env`** — заданы ли `HTTP_OUTBOUND_PROXY`, таймауты и ретраи.
2. **Ручной `curl`** с того же сервера (с тем же прокси, если нужен) — доступен ли URL.
3. **Логи приложения** — строки `CdnFtp outbound fetch: ...` / `CdnFtp outbound fileInfo: ...` при включённом `HTTP_OUTBOUND_LOG_REQUESTS`.
4. **`result.last_curl_error`** / **`info.last_curl_error`** — конкретная причина отказа.
5. **`http_code`** — отличить 404/403 от обрыва соединения.
6. **`attempts`** — сколько раундов реально выполнено (ретраи работают).

## FAQ

**Можно ли задать другой прокси только для одного вызова?**  
Да, передайте `proxy` в объекте `options`.

**Как отключить ретраи?**  
`max_attempts: 1` в `options` или `HTTP_OUTBOUND_RETRIES=1`.

**Чем отличается от `bot.downloadFileFromUrl()`?**  
Скачивание в сценарий бота кладёт файл в хранилище бизнеса. ProxyFetch / `fetchUrlContents` возвращают **строку в памяти**, без сохранения на диск платформы. Кроме того, семантика успеха разная: download требует 2xx, fetch допускает 404.

**Работает ли без прокси?**  
Да: если `HTTP_OUTBOUND_PROXY` пуст и в `options` не передан `proxy`, используется прямой cURL.

**Нужен ли отдельный токен или callback?**  
Нет, это синхронный HTTP-запрос из PHP, без webhook'ов.

## Что дальше

- Перевести `Common.Files.Converter` и `Common.Integrations.Request` на ProxyFetch / `CdnFtp::fetchUrlContents`.
- Перевести `downloadFileFromUrlToBusiness` на общий cURL-раннер (отдельная задача, follow-up).
- При необходимости расширить ядро (заголовки авторизации, POST) отдельной задачей.
- Держать в синхроне значения прокси с Telegram, если на проекте это один и тот же выход в интернет.

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

- Спека: `specs/Task-9703-proxy-fetch-plugin.plan.md`
- Код: `app/Services/CdnFtp.php`, `Plugins/Dynamic/Common/Http/V8Wrapper/ProxyFetch.php`, `config/http_outbound.php`

# Стандарт компонентной и плагинной разработки Metabot v1.0

# Инженерные принципы разработки плагинов и модулей Metabot

## Зачем это нужно

Metabot развивается как платформа, а не как набор разрозненных скриптов, временных обходов и локальных helper-классов. Поэтому для нас критично не просто “написать рабочий код”, а делать это так, чтобы решения можно было повторно использовать, безопасно развивать, тестировать, документировать и передавать между разработчиками без потери смысла.

Этот стандарт нужен, чтобы:

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

Главная идея простая: **мы не пишем код “под случай”, мы строим расширяемую инженерную среду**.

---

## 1. Сначала короткий spec, потом код

### Принцип

Любая новая платформенная доработка начинается с короткого спека: контекст, цель, границы изменения, ограничения, критерии приёмки.

### Почему это важно

Код без предварительной формулировки задачи почти всегда решает не ту проблему, которую кажется, что решает. Спек нужен не ради бюрократии, а ради того, чтобы команда одинаково понимала:

- что именно меняется;
- где граница ответственности;
- что нельзя сломать;
- как проверяется результат.

### Плохо

“Надо быстро подправить два места, чтобы заработало.”

### Хорошо

“Нужно ввести общий outbound HTTP-примитив с proxy/retry и перевести на него конкретные точки, не ломая старые одноаргументные вызовы.”

### Пример

Если появляется системный модуль для исходящих HTTP-запросов, он должен начинаться не с куска кода, а со спека: где он используется, что считается успехом, какие ошибки retryable, как он доставляется в V8 и как тестируется.

---

## 2. Один модуль — одна причина к изменению

### Принцип

У класса, сервиса или плагина должна быть одна понятная причина измениться.

### Почему это важно

Если модуль одновременно:

- делает HTTP-запросы,
- конвертирует CSV,
- сохраняет файлы,
- определяет MIME,
- проверяет политику безопасности,
- генерирует временные имена,

то он уже не является компонентом. Это комбайн. Такие модули плохо тестируются, плохо переиспользуются и становятся центром роста техдолга.

### Плохо

Один `Request`-класс, который “умеет всё”.

### Хорошо

Отдельные компоненты:

- outbound HTTP;
- file transfer;
- CSV utilities;
- event adapters;
- policy/context validation.

### Пример

Компонент голосового ввода должен отвечать за голосовой ввод, а не одновременно за Telegram payload, загрузку аудио, транскрибацию, файловое хранилище и обработку бизнес-сценария.

---

## 3. У каждого компонента должен быть явный контракт

### Принцип

У компонента должны быть:

- понятные входы;
- понятные выходы;
- предсказуемое поведение;
- задокументированная семантика ошибок и ограничений.

### Почему это важно

Компонент без контракта быстро превращается в “магическую штуку”, которую кто-то когда-то написал, а остальные боятся трогать. Это ломает повторное использование и делает развитие платформы случайным.

### Плохо

Метод с названием `getFileInfoByUrl()`, который в одних случаях только возвращает метаданные, а в других ещё скачивает файл, создаёт temp file и вычисляет MIME по содержимому.

### Хорошо

- `fetchUrlContents()` — получает контент;
- `getFileInfoByUrl()` — получает метаданные;
- `downloadFileFromUrlToBusiness()` — скачивает и сохраняет файл в хранилище.

### Пример

Событие `ticket_status_changed` должно явно документировать доступные поля, а не оставлять разработчика угадывать, есть ли там предыдущий статус, текущий статус или только `ticket_id`.

---

## 4. Неявная передача данных запрещена

### Принцип

Компоненты не должны зависеть от скрытых связей, случайных значений в памяти, жёстко заданных путей, доменов, записей в БД или “магии рантайма”, если это явно не является частью контракта.

### Почему это важно

Скрытые зависимости делают код хрупким. Он работает только в головах авторов и в конкретном окружении, но не становится частью платформы.

### Плохо

- жёсткий путь к папке на диске;
- жёстко заданный домен в коде;
- предположение, что JS-обёртка “как-нибудь увидит” PHP-модуль;
- использование значения, которое другой скрипт когда-то где-то записал.

### Хорошо

- пути и домены берутся из конфигурации;
- зависимости передаются явно;
- механизм загрузки модулей описан и воспроизводим;
- мост между PHP, JS и V8 является частью documented delivery model.

### Пример

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

---

## 5. Канальная логика должна быть спрятана внутри компонента

### Принцип

Сценарии и верхнеуровневая логика должны описывать **что происходит**, а не вручную реализовывать Telegram, Max, webhook-переходы, форматы multipart или повторные попытки запросов.

### Почему это важно

Когда канальная и инфраструктурная логика просачивается наружу, платформа перестаёт быть платформой. Она превращается в набор сценариев с вшитой механикой конкретных каналов.

### Плохо

Сценарий сам знает, как устроен Telegram file API, как формируется URL, как грузится файл через прокси и как повторять запрос.

### Хорошо

Сценарий обращается к компоненту:

- `VoiceInput`
- `ProxyFetch`
- `ChannelArtifactResolver`
- `TicketStatusChangeEvent`

### Пример

Если платформа начинает работать и с Telegram, и с Max, и с другими каналами, различия между ними должны жить в адаптерах и компонентах, а не размазываться по V8-скриптам.

---

## 6. Общий платформенный код не должен зависеть от конкретного проекта

### Принцип

Если код претендует на статус общего системного компонента, он должен быть переносимым и пригодным для повторного использования.

### Почему это важно

Компонент, который работает только в одном проекте, на одном домене, в одном окружении или с одним bot ID, не является компонентом платформы. Это проектный helper.

### Плохо

- жёстко заданные пути;
- жёстко заданные URL;
- привязка к конкретному боту, инстансу, бизнесу или окружению;
- хранение инфраструктурной информации внутри логики модуля.

### Хорошо

- конфигурация выносится наружу;
- файловые пути вычисляются через storage layer;
- URL строятся через общие сервисы;
- компонент можно использовать повторно без переписывания.

### Пример

Общий HTTP-модуль — хороший кандидат на платформенный слой. Утилита, которая пишет файлы только в конкретную папку конкретного инстанса, — нет.

---

## 7. Legacy не расширяем дальше как новый стандарт

### Принцип

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

### Почему это важно

Legacy-код может быть полезен как compatibility layer, но если в него продолжать складывать новые обязанности, он становится точкой системного разложения.

### Плохо

Ради новой задачи расширять старый helper ещё сильнее, потому что “он и так уже везде используется”.

### Хорошо

- новый системный слой вводится отдельно;
- старый модуль признаётся legacy;
- миграция идёт постепенно;
- новые доработки делаются на новом стандарте.

### Пример

Если есть старый модуль, который уже умеет и HTTP, и файлы, и CSV, и multipart, это не повод использовать его как базу для новой платформенной логики. Это повод остановить его рост и начать выносить отдельные системные примитивы.

---

## 8. Сначала системный примитив, потом точечная миграция

### Принцип

Повторяющиеся инженерные проблемы должны решаться один раз как системный примитив, а не много раз в разных helper-классах.

### Почему это важно

Когда прокси, retry, таймауты, fallback-логика и диагностика реализуются в разных местах по-разному, это не ускорение, а размножение будущих багов.

### Плохо

- отдельно прокси для Telegram;
- отдельно retry для файлов;
- отдельно download helper в интеграционном плагине;
- отдельно ещё один wrapper “на всякий случай”.

### Хорошо

- единый outbound HTTP-слой;
- единые правила proxy/retry/timeout;
- поверх него — разные политики:
    
    
    - generic fetch,
    - file info,
    - strict file download.

### Пример

Если проблема в `file_get_contents` без прокси возникает в нескольких местах, правильное решение — не “пропатчить ещё одну точку”, а ввести платформенный способ исходящих HTTP-запросов и постепенно перевести туда нужные вызовы.

---

## 9. Границы контекстов должны быть названы

### Принцип

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

### Почему это важно

Без границ всё начинает прилипать ко всему. HTTP начинает знать про файлы, файлы — про каналы, каналы — про бизнес-правила, а бизнес-правила — про способы доставки JS-модулей.

### Плохо

Один модуль сразу “про интеграции”, “про файлы”, “про экспорт”, “про CSV”, “про загрузку”, “про политики” и “про события”.

### Хорошо

Отдельные контексты:

- Outbound HTTP
- File Transfer
- Event Contracts
- CSV/Artifacts
- Channel Adapters
- Runtime Delivery

### Пример

`ticket_status_changed` — это контекст событий и контрактов. `ProxyFetch` — это контекст исходящего HTTP. `downloadFileFromUrlToBusiness()` — это контекст скачивания и сохранения файла. Смешивать это в одну сущность нельзя.

---

## 10. Любая платформенная фича обязана приехать вместе с четырьмя хвостами

### Принцип

Код без доставки и сопровождения не считается завершённой фичей.

### Минимальный комплект

- spec;
- changelog;
- docs;
- тестовый сценарий.

### Почему это важно

Если код есть в git, но:

- runtime его не видит;
- V8 не находит модуль;
- никто не знает, как им пользоваться;
- он не отражён в версии;

то это не готовая функциональность, а полуфабрикат.

### Пример

Новый системный модуль должен:

- быть описан в `specs`;
- попасть в changelog версии;
- иметь документацию по подключению и использованию;
- быть реально проверен на stage.

---

## 11. Перед началом реализации команда должна ответить на пять вопросов

### Обязательные вопросы

1. Какова цель изменения?
2. Где проходит граница модуля или сервиса?
3. Какие контракты меняются?
4. Какие инварианты нельзя нарушить?
5. Как проверяется результат?

### Почему это важно

Если на эти вопросы нет ответа, значит команда ещё не проектирует систему, а только реагирует на симптомы.

### Пример

Если в событие смены статуса добавляется `previous_status_id`, это не “маленькая доработка”. Это изменение event contract. Значит, нужно заранее понять:

- кого оно затронет;
- как это будет документировано;
- что останется backward-compatible;
- как будет выглядеть payload события после изменения.

---

# Практические антипаттерны

## Антипаттерн: “универсальный helper”

### Признаки

- делает слишком много;
- его имя уже не соответствует содержанию;
- в него продолжают складывать новые обязанности;
- его боятся трогать, но продолжают использовать.

### Что делать

- перестать расширять;
- зафиксировать как legacy;
- выделить новые системные примитивы.

---

## Антипаттерн: “разовый фикс становится стандартом”

### Признаки

- решение делалось локально;
- потом на него начинают ссылаться как на общий подход;
- контракта и docs нет;
- оно не готово к повторному использованию.

### Что делать

- либо честно оставить это локальным кейсом;
- либо поднять до уровня полноценного компонента.

---

## Антипаттерн: “доставка не доведена”

### Признаки

- код существует, но среда его не видит;
- модуль доступен только после ручной магии;
- часть живёт в git, часть в БД, часть “должна сама подцепиться”.

### Что делать

- определить единый delivery model;
- сделать загрузку модулей воспроизводимой и проверяемой;
- перестать рассчитывать на “оно, наверное, сработает”.

---

# Merge-checklist для платформенного кода

Перед merge каждый новый платформенный модуль должен пройти проверку:

1. Есть ли короткий spec?
2. Понятна ли одна причина к изменению?
3. Есть ли явный контракт входов и выходов?
4. Нет ли скрытых зависимостей или жёсткого хардкода?
5. Не смешаны ли несколько контекстов в одном модуле?
6. Это системный примитив или проектный helper?
7. Не развиваем ли мы legacy-комбайн вместо нового слоя?
8. Есть ли docs, changelog и stage-сценарий проверки?

---

# Итоговая позиция

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

Наша цель — не просто ускорить написание кода, а построить среду, в которой:

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

**Каждая новая доработка должна отвечать не только на вопрос “как это сделать”, но и на вопрос “где это должно жить как часть системы”.**