Как построить визуальный no-code редактор воронок и AI пайплайнов на React поверх low-code ядра Metabot
Попробовать: https://cjm.metabot24.ru/cjm-designer
Этот урок — про то, как поверх Metabot (который сам по себе полный low-code/full-code backend для чат-ботов и ассистентов) построить новый продукт: визуальный конструктор чат-ботов.
То есть вы создадите реальный продуктовый интерфейс — свой ManyChat / BotHelp, но умнее и современнее.
Главная идея:
Metabot — это low-code/full-code ядро.
Пройдя этот урок, вы научитесь строить целые продукты поверх ядра, создавая собственный фронт, визуальный редактор, AI-функции и конструкторы.
Часть 1. Что будет в этом уроке — и чего здесь не будет
Чтобы вы сразу понимали рамки: этот урок — это фундамент, первая версия конструктора, на котором вы сможете дальше строить полноценный продукт. Здесь мы создаём работающий скелет визуального редактора и настраиваем импорт в Metabot.
А теперь — по пунктам.
Что будет в уроке
1. Создадим собственный JSON-формат воронки
Вы:
-
придумаете свой формат,
-
определите структуру шагов воронки,
-
создадите свои типы команд,
-
добавите свои параметры и опции.
То есть вы спроектируете свой DSL (domain-specific language) — язык описания воронок вашего продукта.
2. Разберётесь, как работает Mapper
Мы подробно посмотрим, как:
-
входящий JSON обрабатывается,
-
шаги проверяются,
-
неподдерживаемые типы фильтруются,
-
нужные команды собираются,
-
создаются переходы, кнопки, логгирование, LLM-вызовы и так далее.
То есть вы поймёте логику трансформации вашего формата → формат Metabot.
Вы получите исходный код Mapper'а.
3. Разберётесь, как работает Builder
Мы детально пройдём по механике:
-
создания секции/папки скриптов в Metabot,
-
создания скриптов,
-
добавления low-code команд в скрипт,
-
создания переходов и menu-кнопок,
-
удаления старых версий воронки.
И главное — вы увидите, как Builder напрямую манипулирует базой данных Metabot (PostgreSQL) через PHP/V8.
Это даёт понимание того, как работает ядро платформы и как можно строить свои надстройки.
Вы получите исходный код Builder'а.
4. Научитесь подключать Metabot как backend
Вы увидите, как:
-
передавать JSON в Metabot через API,
-
запускать импорт,
-
пересобирать воронку целиком,
-
структурировать код для своего продукта.
То есть Metabot превращается для вас в backend-as-a-platform, который вы можете использовать под любые проекты.
5. Сделаем визуальный редактор на React (с помощью V0 + Cursor)
Мы:
-
создадим canvas и интерфейс узлов,
-
свяжем их с JSON,
-
добавим импорт и экспорт в/из файла,
-
добавим базовые настройки шагов и валидации,
-
подготовим front-end архитектуру.
Вы получите:
-
исходный код редактора,
-
архитектуру компонентов,
-
промпты, с помощью которых редактор собирался (это важно — это обучает самостоятельному созданию продуктов).
6. Настроим рабочий цикл разработки
Первые итерации будут такие:
1) создаём JSON вручную →
2) отправляем через Postman →
3) смотрим, как Metabot создал воронку →
4) фиксируем ошибки →
5) делаем свои команды →
6) строим визуальный редактор, который уже генерит нужный JSON →
7) импортируем в Metabot одной кнопкой
Это честный, практичный, продуктовый процесс.
7. Вы получите всё, чтобы создать свой продукт
В итоге у вас будет:
-
работающий визуальный редактор,
-
свой JSON-формат,
- свой набор команд,
- сохранение в файл и загрузка из файла,
-
исходники маппера/билдера,
-
связка React → JSON → Metabot.
На этом можно строить:
-
no-code конструктор,
-
CJM-редактор,
-
AI-конструктор цепочек,
-
бизнесовый продукт поверх Metabot.
🚫 Чего в этом уроке НЕ будет
Чтобы не вводить в заблуждение — сразу фиксируем:
1. Не будет двусторонней синхронизации
Мы делаем только импорт из вашего конструктора → Metabot.
То есть:
-
если воронка существует — она удаляется и создаётся заново,
-
мы не читаем текущую структуру из Metabot,
-
мы не отображаем её обратно в редактор.
Это осознанное упрощение.
Вторую сторону можно сделать, но это отдельный блок задач:
читать данные Metabot → конвертировать → отображать → синхронизировать версии.
Мы это оставляем для следующего урока, если будет запрос.
2. Не будет полноценного менеджмента трафика
Это очень важный момент.
В реальной жизни по ранее импортированной воронке уже могут идти живые пользователи.
Если вы меняете воронку, возникают вопросы:
-
что делать с активными пользователями?
-
надо ли их перенаправлять в новую версию?
-
надо ли блокировать обновления?
-
нужно ли создавать копии воронок?
-
нужно ли делать миграции шагов?
-
как правильно писать стратегию версионирования?
Это целая философия, и у каждого продукта — свой подход.
Мы эту сложность сознательно не включаем в этот урок.
Но если понадобится понадобится помощь — дайте знать, попробуем помочь.
3. Не будет полноценной "платформенности"
В этом уроке:
- нет авторизации и регистрации — импорт по API токену пользователя, привязанному к боту.
- нет совместной работы.
Мы делаем первую версию конструктора, но она уже рабочая и расширяемая.
Принял. Делаю первую итерацию — без фанатизма, но уже структурно, лаконично, красиво, чтобы можно было вложить в документацию и развивать дальше.
Ты дал огромный объём информации, поэтому я сейчас фиксирую каркас главы 2, + делаю первый блок: спецификация команд Metabot + JSON-формат, + простую таблицу команд, красиво оформленную, + мини-описание модели данных (sentences, phrases, references).
В следующей итерации я смогу:
-
добить оставшиеся разделы,
-
оформить финальный CJM JSON на 20 шагов,
-
добавить продвинутую постановку задач.
Глава 2. JSON-формат и фундаментальная модель данных Metabot
Чтобы создать собственный визуальный конструктор поверх Metabot, нужно понимать две вещи:
-
Какие команды реально существуют в ядре платформы
-
Как Metabot хранит эти команды в базе данных
Эта глава даёт разработчику язык конструктора — JSON-формат шага, таблицу команд и модель хранения внутри Metabot.
2.1. Базовая модель данных Metabot
Метабот — low-code движок. Его диалоговая логика физически живёт в трёх таблицах:
sentences — это скрипты
Каждый шаг в нашем редакторе превратится в отдельный скрипт Metabot.
| Поле | Что значит |
|---|---|
id |
ID скрипта |
code |
Уникальный код (мы сами создаём) |
name |
Читабельное имя |
bot_id |
Бот, которому принадлежит скрипт |
section_id |
Папка/категория (наша “воронка”) |
| ... | Остальные поля нам пока не нужны |
Пример:
id: 85681
code: "test-script"
name: "TestScript"
bot_id: 2370
phrases — это команды внутри скрипта
Одна запись = одна команда.
Каждый шаг в нашем редакторе может порождать от 1 до 10 фраз.
| Поле | Описание |
|---|---|
id |
ID команды |
type |
Тип команды (send_text, run_javascript, …) |
sentence_id |
Скрипт, которому команда принадлежит |
content |
Текст или JSON-контент команды |
sort_order |
Порядок выполнения |
alias |
Уникальный ID команды |
Пример вывода send_text:
type: "send_text"
sentence_id: 85681
content: "Hello, World!"
sort_order: 0
Пример ввода пользователя value_input:
id: 231462
type: value_input
sentence_id: 85681
content: {"attribute_key":"var","prompt":"Введите число",...}
sort_order: 1
references — это кнопки (меню)
Меню — это не фраза, а отдельная таблица (так исторически, да). Каждая запись — это кнопка.
| Поле | Значение |
|---|---|
sentence_id |
Скрипт, которому меню принадлежит |
caption |
Текст кнопки |
code |
Код/значение кнопки |
jump_sentence_id |
Куда переходить |
sort_order |
Порядок кнопок |
line_num |
Для многострочных клавиатур |
condition_script_code |
Условие отображения |
Пример:
sentence_id: 85681
caption: "Start Over"
code: "1"
jump_sentence_id: 85680
📌 Все 21 команды Метабота
На момент написания этого урока в Metabot есть 20 команд в скриптах + одна неявная команда (меню).
| № | Команда | Что делает | Где хранится | Пример content в БД |
|---|---|---|---|---|
| 1 | send_text | отправляет текстовое сообщение | phrases | Hello, World!!! |
| 2 | value_input | запрашивает ввод и валидирует | phrases | {"attribute_key":"abc","prompt":"Введите число", ...} |
| 3 | run_javascript | выполняет JS на сервере | phrases | lead.setAttr('abc', 123); |
| 4 | send_image | отправляет изображение | phrases | {"path":"bot/.../logo.png","name":"logo.png"} |
| 5 | send_file | отправляет файл (любые документы) | phrases | {"path":"bot/...","name":"file.pdf"} |
| 6 | run_sentence | выполняет другой скрипт, останавливает текущий | phrases | 85681 |
| 7 | send_email | отправляет email | phrases | {"recipient":"user@mail","subject":"...","body":"..."} |
| 8 | run_trigger | запускает триггер | phrases | {"trigger_id":"7222","run_at":"2025-06-11","run_after_sec":300} |
| 9 | set_lead_status | меняет статус лида | phrases | 7783 |
| 10 | add_lead_tags | добавляет теги | phrases | "tag" |
| 11 | remove_lead_tags | удаляет теги | phrases | "tag" |
| 12 | nlp_detect_intent | определяет интент | phrases | {"text_to_detect":"intent"} |
| 13 | run_js_callback | делает JS-callback | phrases | {"prompt":"...","error_message":"...","js_code":"..."} |
| 14 | async_api_call | async вызов API (например, обращение к LLM) | phrases | {"js_code":"...","is_immediate_call":1} |
| 15 | add_lead_contexts | добавляет контекст | phrases | "context" |
| 16 | remove_lead_contexts | удаляет контекст | phrases | "context" |
| 17 | forward_to_operator | переводит на оператора | phrases | 7784 |
| 18 | return_to_bot | возвращает в бота | phrases | 7783 |
| 19 | repeat_sentence | повторяет текущий скрипт | phrases | {} |
| 20 | stop | полностью останавливает выполнение | phrases | {} |
| 21 | menu (reference) | кнопки меню, переходы, inline actions | references | caption="Start", jump_sentence_id=85680 |
Палитра команд платформы:
Короткое объяснение логики исполнения команд
-
Команды внутри одного скрипта (
sentence) выполняются сверху вниз поsort_order. -
Любая команда типа
run_sentenceилиrun_javascriptможет мгновенно передать управление в другой скрипт. -
Кнопки (
references) — это отдельная сущность: они не лежат вphrases, но участвуют в сценарии. В интерфейсе меню расположено внизу скрипта, после списка команд.
«Зачем нам свой конструктор и свои команды»
После того как мы разобрались, какие команды реально существуют внутри Metabot и посмотрели, как они физически хранятся в базе (sentences, phrases, references), мы можем перейти к ключевой идее нашего урока:
Мы не будем создавать интерфейс, который один-в-один повторяет Metabot.
Мы создадим свой собственный визуальный конструктор, и в нём у нас будут свои команды — более удобные, более компактные и намного более выразительные.
Почему так?
Почему в Metabot всё разбито на 21 команду
(и почему это отлично для движка, но неудобно для дизайнера чат-ботов)
Metabot — это low-code ядро. Каждая команда — это минимальная атомарная операция:
-
отправить текст,
-
отправить картинку,
-
записать тег,
-
выполнить JS,
-
перейти в скрипт,
- дождаться ввода
и так далее.
Это правильно для движка:
движок должен быть предсказуемым и детерминированным.
Но для человека, который рисует сценарий, это слишком низкий уровень.
Чтобы собрать один «узел» диалога, дизайнеру нужно вручную собрать:
-
1–3
send_text-
условия и JS
-
настройки под разные каналы
-
кнопки меню (в
references)
-
обработчики кликов (ещё несколько
run_sentence+ JS)
-
JS для логирования для аналитики
-
триггеры для отслеживания ссылок
-
переходы к следующему шагу
-
В итоге один визуальный узел превращается в:
→ 6–12 низкоуровневых фраз в базе
→ 2–4 скрипта (sentences)
→ и пачку записей в references
Именно поэтому мы не хотим заставлять пользователя руками разбираться в 21 команде ядра. Мы хотим сократить этот слой.
Теперь проектируем наш собственный редактор
Мы создаём свою систему узлов — такую, в которой:
-
один узел = одна логическая операция сценария,
-
пользователь видит высокоуровневую настройку,
-
а всё сложное — «распаковывается» автоматически внутрь Metabot при импорте.
Это и есть смысл маппера и билдера.
Какие наши команды мы сделаем первыми
1. Start / Entry Node
Стартовый узел воронки.
Даёт визуальную точку входа + будущую связь с диплинками.
Вход в воронку возможен как из других воронок / сценариев, так и снаружи при переходе в мессенджер из приложений или из контента в Интернет.
Пример шага в новом no code редакторе:
Пример настроек в новом no code редакторе:
Пример JSON-кода шага:
{
"code": "entry_point_main",
"type": "entry_point",
"name": "Старт",
"next_step": "log_action_2ec8cbbe",
"deep_links": [
{
"code": "waylogger",
"title": "New Deep Link",
"active": true,
"parameters": [
{
"key": "ref_source",
"value": "vk_ads"
},
{
"key": "source",
"value": "cpc"
},
{
"key": "campaign",
"value": "waylogger"
},
{
"key": "content",
"value": "1"
}
]
}
],
"coordinates": {
"x": 650,
"y": 728
}
}
2. Send Text (расширенная)
Это не просто send_text из Metabot.
Наша версия включает:
-
💬 универсальное сообщение
-
💬 кастомизация сообщения под каналы (Telegram, WhatsApp, VK, WebChat и так далее)
- 🔘 поддержка кнопок
- ⚙️ обработчики событий нажатия на кнопку
-
📌 запись в аналитику прямо внутри узла
- 🔗 поддержка ссылок (с автогенерацией триггеров)
В Metabot для этого потребовалось бы от 6 до 12 низкоуровневых команд.
У нас — один визуальный блок.
Пример шага в новом no code редакторе:
Форма редактирования стандартной команды "Отправить текст" в low-code редакторе:
Форма редактирования новой команды "Отправить текст" в новом no-code редакторе, сравните возможности:
JSON:
{
"code": "send_text_27607f82",
"type": "send_text",
"content": "3. Кто вы?",
"next_step": "log_action_a47418f3",
"buttons": [
{
"title": "Владелец проекта",
"next_step": null,
"input_code": "1",
"js_condition": "",
"value": "Владелец проекта",
"add_tags": [],
"remove_tags": []
},
{
"title": "Маркетолог",
"next_step": null,
"input_code": "2",
"js_condition": "",
"value": "Маркетолог",
"add_tags": [],
"remove_tags": []
},
{
"title": "Разработчик",
"next_step": null,
"input_code": "3",
"js_condition": "",
"value": "Разработчик",
"add_tags": [],
"remove_tags": []
},
{
"title": "Другое",
"next_step": null,
"input_code": "4",
"js_condition": "",
"value": "Другое",
"add_tags": [],
"remove_tags": []
}
],
"buttons_value_target": {
"scope": "lead",
"key": "role"
},
"log_way_steps": [
{
"type": "step",
"way": "waylogger",
"step": "role_selected",
"event": "",
"tag": "",
"tag_action": "add",
"utter": ""
}
],
"coordinates": {
"x": 197,
"y": 1044
}
}
3. Записать в аналитику
Под "капотом" это будет метаботовская команда run_javascript в которой будет вызов плагина аналитики WayLogger.
Мы прячем всё JS-пекло от пользователя.
В нашем интерфейсе есть:
-
выбор типа логирования:
step,event,tag,utter, -
выбор пути (way),
-
параметры.
JSON:
{
"code": "log_action_a47418f3",
"type": "log_action",
"log_type": "step",
"way": "waylogger",
"step": "finished_quiz",
"event": "",
"tag": "",
"tag_action": "add",
"utter": "",
"next_step": "send_pdf",
"coordinates": {
"x": 474,
"y": 1142
}
}
4. Переход к следующему шагу
В Metabot это команда run_sentence.
В нашем редакторе — просто стрелка между узлами.
А в JSON это параметр шага, кнопки и так далее, например, next_step.
5. Обращение к LLM (call_llm)
В Metabot — большой JS код с настройками в команде async_api_call. Например:
const LLMClient = require("Common.MetabotAI.LLMClient")
const llm = new LLMClient("UserDialog")
llm.setProvider("OpenAI")
llm.setModel("gpt-3.5-turbo")
llm.setErrorScript("SupportBot:ErrorFallback")
llm.setPromptTable("gpt_prompts", "SupportBot")
if (isFirstImmediateCall) {
llm.addSystemPrompt(`Ты должен ответить пользователю, используя найденную информацию из базы знаний.`)
llm.addSystemPrompt(
`Если в базе знаний будут найдены ссылки на медиа контент, `+
`определи релевантность контента, проанализировав их описание, и включи релевантные ссылки в формате markdown. ` +
`Сам ссылки не придумывай!`)
llm.addSystemPrompt(`Информация из базы: {{$kb_chunks}}`)
llm.addSystemPrompt(`Распознаннный интент: {{$user_intent}}`)
llm.addHistoryToPrompts() // Добавляем историю
llm.addUserPrompt(`{{$user_input}}`)
llm.addSystemPrompt(`$UserDialog:final`) // Промпт агента из таблицы
llm.addSystemPrompt(`@общий`) // Общий промпт из таблицы
llm.prepareRequest()
return llm.sendRequest()
}
llm.handleResponse()
llm.sendFormattedResponse()
return true
Это не самый свежий пример кода, но рабочий. Здесь мы делаем:
-
выбор провайдера и модели
- error / fallback узел
- устанавливаем агента и подключаем таблицу промптов
- system / user prompts
-
способ отображения ответа
-
параметры истории
- отправляем запрос, принимаем, выводим
Всё это — внутри одного команды и требует базового владения JS кодом.
В новом редакторе упакуем все это в один no code узел:
И одну форму, чтобы пользователю не пришлось писать JS код — мы его будем генерить за него при импорте в low-code:
JSON:
{
"code": "call_llm_ccbfc9c0",
"type": "call_llm",
"title": "Формируем ответ LLM",
"agent_name": "SupportBot",
"provider": "OpenAI",
"model": "gpt-3.5-turbo",
"prompt_table": "gpt_prompts",
"system_prompts": {
"start": [
"Ты должен ответить пользователю, используя найденную информацию из базы знаний.",
"Если в базе знаний будут найдены ссылки на медиа контент, \nопредели релевантность контента, проанализировав их описание, и включи релевантные ссылки в формате markdown. \nСам ссылки не придумывай!",
"Информация из базы: {{$kb_chunks}}",
"Распознаннный интент: {{$user_intent}}"
],
"final": [
"$UserDialog:final",
"@общий"
]
},
"history": {
"enabled": true,
"save_to_attr": "chat_history_str",
"max_length": 4,
"add_to_prompts": true
},
"user_query": {
"enabled": true,
"attr": "user_input",
"add_to_prompts": true
},
"response": {
"enabled": true,
"save_to_attr": "user_intent",
"display_to_user": true,
"format": "markdown"
},
"trace_enabled": false,
"next_step": "search_kb_394b4ae2",
"error_step": "send_text_47451106",
"coordinates": {
"x": -336.29289321881345,
"y": 2063.166664123535
}
}
6. Поиск по базе знаний (search_knowledgebase)
Аналогично — в Metabot при импорте создаем JS команду и необходимые связки.
let userIntent = lead.getAttr('user_intent')
// Если интент есть — ищем по базе знаний
if (userIntent) {
const KnowbaseSearch = require('Common.MetabotAI.KnowbaseSearch')
const kbSearch = new KnowbaseSearch()
const chunks = kbSearch.findBestChunks("DefaultKB", userIntent, "default")
// Сохраняем все, что нашли в kb_chunks
lead.setJsonAttr("kb_chunks", chunks)
bot.sendMessage(`chunks: ${JSON.stringify(chunks)}`)
} else {
lead.setJsonAttr("kb_chunks", [])
}
А в no code — один узел, один JSON.
Обратите внимание у узла мы добавили три выхода: успешный, ничего не найдено и ошибка.
JSON:
{
"code": "search_kb_394b4ae2",
"type": "search_knowledgebase",
"knowbase_name": "DefaultKB",
"query_attr": "user_intent",
"domain": "default",
"save_results_to_attr": "kb_chunks",
"trace_enabled": false,
"next_step": "call_llm_417fc51a",
"not_found_step": "send_text_455c4afe",
"error_step": "send_text_47451106",
"coordinates": {
"x": -337,
"y": 2417
}
}
Почему наши команды будут «упакованными»
(концепция compact high-level commands)
Каждая наша команда — это высокоуровневый «комбинированный» блок.
Когда пользователь его настраивает, мы:
-
собираем данные в простой JSON (наш формат),
-
передаём этот JSON мапперу,
-
маппер превращает один наш узел в:
-
1 или несколько
sentences -
десятки
phrases -
references -
JS-код
-
триггеры
- диплинки
-
переходы
-
аналитику
- и так далее
-
То есть пользователь видит простую, компактную визуальную воронку, а под капотом строится настоящая сложная система Metabot с триггерами, скриптами, условиями, логированием и разветвлёнными сценариями.
Экономический эффект / ROI
Чтобы собрать один логический узел вручную, в Metabot приходится склеивать десятки микродействий: несколько сообщений, кнопки, переходы, теги, аналитику, JS-обработчики, триггеры, условия. Каждое из этих действий занимает секунды или минуты. А в реальном проекте — это часы.
Чем сложнее сценарий, чем массивнее продукт — тем больше времени уходит не на смысловую работу, а на рутину: перенести текст, добавить теги, прописать переходы, подправить сортировку, создать скрипт, связать кнопки. Всё это множится в геометрической прогрессии.
И вот здесь становится очевиден смысл no-code-редактора.
Один визуальный узел в нашем редакторе может вместить всю эту низкоуровневую сложность: аналитику, кнопки, ссылки, условия, LLM-вызовы, JS-код, обработку ввода, переходы — всё внутри одного компактного блока с настройками.
Пользователь просто выбирает тип узла, заполняет пару полей — и весь сложный набор действий раскладывается движком автоматически.
Вместо десятка операций — одна.
Вместо нескольких минут — несколько секунд.
Вместо часов — десятки минут.
На дистанции это превращается в огромную экономию ресурсов.
За неделю — это часы.
За месяц — дни.
За год — целые недели чистого рабочего времени, которые раньше уходили на рутину.
И это уже прямой экономический эффект:
команды создают продукты быстрее, дешевле, рентабельнее;
клиенты получают большую ценность за те же деньги;
а вы можете делать более сложные решения теми же силами.
При этом мы сохраняем лучшее из двух миров:
-
Скорость и простоту no-code для типовых сценариев
-
Гибкость low-code/full-code ядра Metabot для любых нестандартных кейсов
Нужна свобода — подключили разработчика и дописали кастомный JS или сложную логику.
Нужна скорость — делаем всё в визуальном редакторе.
И это не все: наш JSON-формат позволяет генерировать воронки автоматически.
Вы можете написать в ChatGPT: «Сгенерируй мне воронку для такого-то кейса» — и импортировать её одним кликом.
Та самая воронка на скриншоте в начале этого урока, состоящая из восьми шагов, создаётся в 5–10 раз быстрее, чем через классический low-code.
Это и есть настоящая эффективность:
быстрее создавать — быстрее тестировать — быстрее зарабатывать.
JSON-формат нашего визуального конструктора
После того, как мы разобрались с тем, как устроены низкоуровневые команды Metabot и почему нам нужен собственный слой абстракции, самое время поговорить о JSON-формате, который станет языком нашего визуального редактора.
По сути, мы создаём свой диалоговый DSL/язык доменной области — простой, человекочитаемый, легко генерируемый (вплоть до того, что его можно создавать через ChatGPT). Этот формат затем преобразуется в структуры Metabot: скрипты, команды, переходы, кнопки, аналитику, LLM-вызовы.
Чтобы понимать, как его проектировать, нужно осознать два ключевых отличия от классического low-code формата Metabot.
1. В чём принципиальное отличие от формата Metabot
✔ В Metabot каждая логика — это скрипт, который содержит много команд
Скрипты связаны переходами (run_sentence), а внутри одного скрипта команды выполняются последовательно — как патроны в обойме.
Это отличный формат для движка, но он низкоуровневый и слишком детализированный для проектирования диалогов.
✔ Наш новый формат — это машина состояний
Мы мыслим не скриптами, а шага́ми.
Каждый шаг — это одно состояние, из которого есть переходы в другие состояния.
Похожая логика используется в BPMN, state-machine диаграммах, автоматах Мили/Мура.
Внутри шага могут происходить десятки внутренних действий, но в JSON это будет один объект.
Таким образом:
-
Metabot = «список команд, выполняющихся последовательно»
-
Новый формат = «автомат состояний»:
step → step → step
Этот подход радикально упрощает визуальное проектирование и делает структуру понятной.
2. Общая структура JSON-формата
Мы храним всю воронку в одном JSON-документе, который содержит:
1) Параметры проекта (bot id, версия, формат и т.д.)
2) Массив steps — список шагов машины состояний
Вот упрощённая структура:
{
"script_request_params": {
"format": "cjm",
"version": "1.0",
"bot_id": 2370,
"code": "quiz_lead_magnet_tags",
"title": "Воронка WayLogger",
"channels": {
"telegram_bot_name": "metabot",
"whatsapp_phone_number": "79150465850",
"vk_group_name": "metabot_platform",
"use_chat_widget": true
},
"steps": [ ... ]
}
}
Обращаем внимание на обёртку script_request_params без которой платформа Metabot "не услышит" передаваемые данные.
3. Зачем нужен «format» и «version»
Мы сознательно включаем поля:
-
format: имя формата (например,cjm) -
version: версия схемы
Это позволяет:
✔ поддерживать совместимость старых воронок
Если формат JSON изменится (а он будет меняться), старые импорты не сломаются.
✔ нескольким продуктам использовать разные форматы
Один редактор → один формат.
Другой редактор → другой формат.
Все они могут жить в одной экосистеме Metabot.
✔ при импорте определять, как парсить данные
Mapper видит format + version и понимает, какой алгоритм применять.
4. Название и код воронки
Поля:
-
code— машинное имя воронки -
title— человекочитаемое название
В метаботе мы формируем имя секции:
cjm:1.0:quiz_lead_magnet_tags:Воронка WayLogger
Секция (папка) создаётся заново при каждом импорте, что даёт чистый, предсказуемый результат.
5. Массив шагов steps — сердце машины состояний
Каждый шаг описывается JSON-объектом:
-
уникальный
code(имя узла) -
тип шага (
send_text,analytics,llm_call, …) -
переходы в другие шаги
-
кнопки
-
аналитика
-
делегируемые действия (напр. отправить файл, записать теги)
-
координаты (для визуального редактора)
-
любые дополнительные поля
Идея проста:
Один наш шаг = целый набор низкоуровневых команд Metabot.
Визуально шаг выглядит как блок на диаграмме (см. скриншот), а в импорте он превращается в цепочку скриптов и команд.
6. Как мы создаём скрипты и команды при импорте
Во время импорта Metabot получает:
-
format -
version -
code(воронки) -
steps(шаги)
Mapper делает следующее:
✔ 1. Находит и удаляет старую cекцию/папку со всеми скриптами
(имя: format:version:code:title)
✔ 2. Создаёт новую секцию
Внутрь неё будут помещаться все скрипты.
✔ 3. Для каждого шага создаёт один или несколько скриптов
Количество зависит от сложности шага:
-
send_text→ 1 скрипт -
send_text + buttons + analytics→ 1–3 скрипта -
и так далее.
✔ 4. Формирует уникальные коды скриптов
Это ключевой момент.
Мы склеиваем:
<format>:<version>:<сjm_funnel_code>:<step_code>
Например:
cjm:1.0:quiz_lead_magnet_tags:entry_point_main
cjm:1.0:quiz_lead_magnet_tags:hello_intro
cjm:1.0:quiz_lead_magnet_tags:send_pdf
cjm:1.0:quiz_lead_magnet_tags:log_action_2ec8cbbe
Так обеспечивается:
-
уникальность внутри всего бота
-
читаемость
-
возможность переимпорта
-
возможность отладки
✔ 5. Внутрь каждого скрипта складывается набор команд (phrases)
Например, шаг «Send Text» превратится в:
-
send_text run_sentence(переход)-
кнопки в
references -
еще один скрипт с
run_javascript(аналитика)
7. Почему шагам обязательно нужен уникальный code
Каждый узел должен иметь уникальный идентификатор.
Без него невозможно:
-
связать узлы между собой
-
построить диаграмму
-
обновлять узлы
-
делать автогенерацию
-
однозначно создать скрипты в Metabot
Пользователь может менять коды шагов — но внутри маппера этот код всегда станет частью полного имени скрипта.
8. Что происходит со старой версией воронки
В текущей версии урока мы применяем упрощённый алгоритм:
✔ удалить старую секцию полностью
✔ создать новую с тем же именем
✔ импортировать шаги заново
Это быстрый, предсказуемый, безопасный метод.
Если нужно:
-
версионность → меняем
version -
несколько версий воронки → клонируем формат
-
продвинутый sync → пишем собственный маппер (за пределами этого урока)
9. Что нужно, чтобы импортировать воронку
Чтобы импортировать JSON:
1. Создаём пользователя с API-доступом
TODO: как это сделать — в отдельном мануале. Обратитесь в поддержку, если здесь все еще нет ссылки и поиск по документации не увенчался успехом.
2. Создаём endpoint в боте
В разделе Internal API создаём endpoint, например, с таким алиасом:
cjm/import
3. Кладём внутрь endpoint JS-код:
const { getSuccessResponse, getErrorResponse } = require('Common.Utils.Response')
const { Mapper } = require('Common.CJM.Mapper')
const {
id = null,
format,
version,
bot_id,
code,
title,
steps = []
} = request.json || {}
if (!format || !version || !bot_id || !code || !title) {
return getErrorResponse("Missing required fields: format, version, bot_id, code, title")
}
try {
const mapper = new Mapper()
result = mapper.runImport(bot_id, format, version, code, title, steps)
return getSuccessResponse({result})
} catch (error) {
return getErrorResponse(`Error saving CJM: ${error.message}`)
}
Всё. Теперь можно импортировать воронки из:
-
Postman
-
вашего React/Vue редактора
-
ChatGPT (сгенерировали JSON → вставили → импортировали)
🔷 Глава 2 завершена.
Следующая — Глава 3. Import: Mapper и Builder
ВТеперь, нейкогда мы разберём:разобрали сам формат JSON-схемы, можно перейти к механике её импорта в Metabot. На платформе в Common.CJM доступны два ключевых плагина, которые вместе образуют мост между вашим JSON и реальными скриптами в базе данных:
Mapper (JavaScript) — читает JSON, создаёт структуру карты, генерирует команды и меню.
Builder (PHP) — низкоуровневый слой, который напрямую создаёт секции, скрипты, команды и переходы в базе.
Эти плагины доступны всем пользователям платформы, но важно понимать их статус:
-
какОбщийработаетMapperмаппери Builder — это обучающие версии, не полноценный коммерческий конструктор.
Они упрощены, без множества проверок и UI-логики. Их задача — показать принципы, на которых строится no-code. -
Исходники мы даём прямо в статье — вы можете взять их за основу и создать свой собственный конструктор.
На общем сервере Metabot нельзя ставить новые PHP-плагины (по соображениям безопасности), но на выделенном сервере или коробочной установке — можно.
Как работает связка Mapper → Builder
Чтобы показать структуру максимально практично, мы дальше будем двигаться блок за блоком:
показываем фрагмент кода → объясняем, что он делает → даём рекомендации, где можно улучшить.
Но вначале — краткая логика работы всей цепочки.
1. Устанавливаем контекст: выбор бота и проверка доступа
Mapper — это JS-плагин, но он работает через Builder, а Builder — PHP-уровень, который:
проверяет принадлежность бота к бизнесу;
отслеживает попытки добавления скриптов в чужой бот;
обеспечивает гарантии целостности данных.
Поэтому первым шагом Mapper вызывает:
setBotById(botId)
Если бот не относится к текущему бизнесу — импорт прерывается.
Это фундаментальный слой безопасности, и Builder строго следит, чтобы вы не могли повредить данные другого бота.
2. Создаём секцию под карту
При импорте мы:
формируем префикс (format:version:mapCode),
удаляем предыдущую секцию целиком,
создаём свежую секцию с чистой структурой.
Это важно, потому что карта имеет жёсткую целостность:
скрипты, переходы, обработчики кнопок, логирование — всё должно меняться согласованно.
3. Почему импорт идёт в три слоя, а не линейно?
Вот здесь та часть, которую многим разработчикам сложно понять.
Нельзя просто:
взять шаг,
создать под него скрипт,
добавить команды,
добавить кнопки,
создать переходы.
Почему?
Потому что кнопки и переходы требуют ID-скриптов, а ID знает только Builder, и только после создания всей секции.
Поэтому алгоритм у нас такой:
Шаг А — создаём скрипты.
Это даёт нам ID каждого шага.
Шаг Б — наполняем команды.
Включая аналитику, custom script, call_llm, search_kb.
Шаг В — собираем меню (кнопки).
Каждая кнопка — это либо прямой переход, либо создание отдельного скрипта-обработчика.
Шаг Г — собираем переходы.
Все переходы хранятся временно в массиве transitions,
и только в самом конце добавляются в базу.
Именно это обеспечивает корректную топологию карты.
Этот подход можно усложнить и оптимизировать,
но важно понимать, что «слоистость» — не прихоть, а необходимость, если мы хотим:
избежать циклических зависимостей;
избежать ситуаций, когда ссылка создаётся раньше объекта;
сохранять атомарность структуры карты.
Исходный код Маппера и Билдера
Полный код Mapper и Builder из этого урока:
Понял.
Тогда сейчас делаем Часть 2: подробный разбор кода, начиная строго с Builder, как отдельного плагина.
Я подготовил структурный, понятный, очень технически точный разбор Builder (PHP): что он делает, зачем нужен, какие методы есть, какие гарантии даёт, как он взаимодействует с JavaScript, и почему вся архитектура построена именно так.
ЧАСТЬ 3.1 — Builder (PHP): низкоуровневое ядро конструктора
Builder — это низкоуровневый плагин, который напрямую работает с таблицами Metabot:
sentences — скрипты;
phrases — команды внутри скриптов;
script_sections — секции (папки);
references — меню (кнопки переходов);
botlog — логи.
Mapper на JavaScript работает «поверх» Builder’а, но именно Builder делает грязную работу:
✔ создаёт реальные записи в БД
✔ удаляет секции и весь вложенный контент
✔ проверяет политику доступа
✔ обеспечивает целостность ссылок
✔ создаёт команды и меню
✔ соединяет скрипты между собой
Builder — это ваш «микро ORM + сервис создания бота».
Mapper — лишь удобная оболочка.
Зачем Builder существует вообще?
Потому что:
V8-движок Metabot может выполнять JavaScript, но не имеет прямого доступа к БД;
PHP — серверное ядро платформы, которое защищено, логирует действия и выполняет транзакции;
Builder — это bridge между JS и PHP, через V8 Wrapper.
Mapper вызывает Builder так:
const Builder = require('Common.CJM.Builder')
Методы Builder становятся доступными JS-коду.
Архитектура Builder
Builder хранит три ключевых контекста:
protected ?Business $_business = null;
protected ?Bot $_runFromBot = null;
protected ?Bot $_bot = null;
protected int $_botId;
protected ?Lead $_lead = null;
Зачем?
Потому что Builder не должен позволять JS-коду выкрутить руки бизнес-логике:
вы не можете создавать скрипты в чужом боте;
вы не можете добавлять команды в скрипт другого бизнеса;
вы не можете обращаться к несуществующему боту;
удаление секции может произойти только в рамках бизнеса, которому она принадлежит.
Это критично.
🔐 CheckPolicy(): сердце безопасности
Каждый метод Builder начинает с этого:
$this->checkPolicy();
Он проверяет:
передан ли текущий бизнес
передан ли текущий бот
принадлежит ли бот действующему бизнесу
корректен ли lead
корректна ли топология
установлен ли botId
Без этого Builder вообще не работает.
Методы Builder: полный разбор
Теперь — весь API Builder, который нам нужен для маппера и для создания собственного конструктора.
1) setBotById(botId)
Устанавливает текущий бот для всех дальнейших операций.
Почему нельзя делать это в JS напрямую?
Потому что выбор бота — потенциальная точка атаки.
Метод делает:
ищет Bot
проверяет, что bot.business_id принадлежит текущему бизнесу
сохраняет _bot и _botId
Возвращает:
['success' => true, 'bot_id' => X]
2) createSection(sectionTitle)
Создаёт папку (ScriptSection) для новой карты:
bot_id
name (полное имя "format:version:mapCode:mapTitle")
сортировка = 0
Builder не создаёт вложенность, всё плоское.
Вложенность симулируется именованием.
3) deleteSectionByCodeDeep(sectionCode)
Самый опасный и самый важный метод.
Удаляет всю секцию по коду и все её вложенные элементы.
Пример: для карты с кодом testmap удалит всё, что содержит "testmap".
Делает:
ищет section
удаляет все скрипты в этой секции
удаляет все фразы этих скриптов
удаляет все меню (reference)
удаляет все переходы в run_sentence, которые ссылались на эти скрипты
какудаляет билдер создаёт фразылоги
какудаляет связываютсясаму узлысекцию
Это глубокая очистка, без которой импорт JSON невозможен.
Логи приходится удалять, потому что иначе Metabot не даст удалить скрипты из-за целостности БД. Это не очень хорошая практика т.к. по воронке могли пройти лиды и логи нужны. Это к вопросу, который мы поднимали выше, что простого одностороннего импорта, удаляющего старую версию, в полноценном продукте не достаточно. Для урока мы сознательно упрощаем.
4) createScript(sectionId, code, name)
Создаёт пустой скрипт в конкретной секции:
sentence.bot_id = this->_botId
какsentence.section_id формируется= finalsectionId
sentence.code = code
sentence.name = name
ГотовВозвращает:
['id' => sentence.id, 'code' => sentence.code]
Это строительный блок №1 для Mapper.
5) findScript(code)
Возвращает скрипт или null.
Mapper использует это для проверок.
6) deleteScript(code)
Удаляет скрипт (предложение) + все фразы.
7) createCommand(scriptCode, type, content, commandCode, sort_order)
Создаёт команду (фразу) внутри скрипта.
Поля:
sentence_id — скрипт
type — тип команды (send_text, run_javascript, run_sentence…)
content — JSON или строка
alias — идентификатор команды (UUID либо переданный)
sort_order — порядок
Это фундаментальная часть, потому что Mapper весь low-code строит только на этих операциях.
8) existsScript / existsCommand
Проверяют существование.
Mapper использует для валидаций.
9) deleteCommand(scriptCode, commandCode)
Удаляет фразу с нужным alias.
Обратите внимание, что в low-code интерфейсе конструктора у команд нет алиасов, а в БД есть.
10) createMenuItem()
Создаёт кнопку в меню:
Поля:
sentence_id — скрипт, где кнопка находится
jump_sentence_id — скрипт, куда ведёт
caption — текст кнопки
code — иногда это input_code
sort_order — порядок кнопки
line_num — расположение ряда (для Telegram клавиатур)
js_condition — условие показа
Mapper формирует их в конце, когда все ID известны.
11) findSectionByCode / findSectionByTitle
Используются для поиска секций перед удалением.
12) deleteSectionByIdDeep
Глубокое удаление секции (аналогично deleteSectionByCodeDeep).
Именно этот метод делает большую часть работы:
удаляет скрипты, фразы, меню, логи
удаляет переходы между скриптами
очищает всё до пустого состояния
В сумме Builder умеет:
1) Управлять секциями
создавать, искать, удалять с зависимостями
2) Управлять скриптами
создавать, искать, удалять, проверять существование
3) Управлять командами
создавать, удалять, находить
4) Управлять меню
создавать кнопки и переходы
5) Очищать полностью старую карту
deep delete секции
6) Гарантировать безопасность через CheckPolicy
никаких операций вне текущего бизнеса
Итого о Билдере
Builder — это низкоуровневый API конструктора Metabot, на котором можно собрать:
no-code редактор
low-code редактор
визуальный CJM editor
auto-generator воронок из JSON, GPT, CRM
свой собственный mini-n8n или BotHelp-лайк движок
Mapper лишь использует Builder, но Builder — это фундамент.
🔎 Примечание о расширении Builder
Если вам понадобится добавить в Builder новый функционал — вы можете это сделать, но важно учесть следующее:
PHP-плагины запрещены на общем облачном сервере Metabot в целях безопасности.
Поэтому собственные версии Builder можно устанавливать только:
на выделенном сервере, или
в коробочной (on-premise) версии платформы.
Если вам требуется расширить Builder именно под свой продукт, вы можете создать собственный плагин Builder и развивать его так, как вам нужно. Это полностью поддерживается на выделенных/коробочных установках.
Если же вы хотите предложить улучшение или новый метод в общий Builder, вы можете прислать ваш вариант реализации или концепцию в поддержку Metabot.
Мы рассмотрим предложение и, если оно подходит под архитектуру платформы, подумаем о включении в будущие версии общего плагина.
Подробнее о возможностях выделенных серверов и коробочных лицензий смотрите здесь:
https://metabot24.ru/price/tarify-na-platformu/
3.2 — Маппер: подробный разбор архитектуры и алгоритма импорта
Маппер — это верхнеуровневый JS-плагин, который принимает JSON-описание вашей схемы (воронки) и преобразует её в реальную структуру скриптов Metabot через Builder. Mapper не работает с базой напрямую — он опирается на PHP-плагин Builder, а сам выполняет только логику трансформации структуры шагов, генерацию JavaScript для команд и построение связей.
Маппер вы можете разместить в плагины вашего бизнеса и использовать на любом сервере, включая общий облачный. Либо можете использовать нашу последнюю версию из Common.CJM.Mapper.
Основная ответственность Mapper:
Проверить формат и подключить нужного бота
Создать секцию (папку) под воронку
Создать скрипты под каждый шаг
Наполнить эти скрипты командами
Сгенерировать переходы (default/аналитика/кнопки)
Сгенерировать и подключить JS-команды: LLM, KB-поиск, кастомный скрипт, логгер
Построить меню-кнопки и обработчики
Добавить все переходы между скриптами
Далее — полный разбор устройства Mapper с примерами кода и объяснением, зачем всё так устроено.
3.2.1. Подключение Builder и базовая инициализация
Mapper работает в JS, но Builder — это PHP-плагин. Их соединяет V8-wrapper:
const Builder = require('Common.CJM.Builder')
class Mapper {
constructor() {
this.builder = Builder
}
Builder подгружается как JS-объект, но внутри вызывает PHP-код.
Перед началом импорта Mapper обязан:
Установить бота
Пройти CheckPolicy внутри Builder (безопасность)
setBotById(botId) {
const result = this.builder.setBotById(botId)
if (result.success) {
this.botId = botId
}
return result
}
Если бот не принадлежит бизнесу, Builder возвращает ошибку. Это важно: так мы не позволяем создать скрипт в чужом боте.
3.2.2. Главный метод: runImport()
Это сердце Mapper. Он получает JSON из внешнего API:
runImport(botId, importFormat, importVersion, mapCode, mapTitle, steps = [])
Шаг 1. Установка бота
Если бот не найден или доступ запрещён — бросаем ошибку:
const { success, message } = this.setBotById(botId)
if (!success) throw new Error(message)
Шаг 2. Генерация префикса и названия папки
Маппер работает по строгой системе кодирования:
{format}:{version}:{mapCode}:{stepCode}
Папка:
cjm:1.0:quiz_lead_magnet_tags:Воронка WayLogger
Это ключевой принцип: каждый объект (скрипт, команда и др.) в Metabot получает уникальный код, который невозможно перепутать между воронками.
const mapPrefix = `${importFormat}:${importVersion}:${mapCode}`;
const sectionTitle = `${mapPrefix}:${mapTitle}`
Шаг 3. Удаление старой секции
Mapper не обновляет кусочно — он делает полную пересборку:
this.builder.deleteSectionByCodeDeep(mapCode)
Если вам нужна версия 1.0 и 1.1 одновременно — просто укажите другую version перед импортом.
Шаг 4. Создание новой секции
const section = this.builder.createSection(sectionTitle)
const sectionId = section.id
3.2.3. Фаза 1 — Создание скриптов (без наполнения)
Это важнейший архитектурный момент:
Мы не можем создавать команды или кнопки, пока не созданы ВСЕ скрипты.
Почему?
Потому что кнопка указывает на next_step, а Builder требует ИД скрипта — а его нет, если скрипт ещё не создан.
Поэтому первая фаза:
for (const step of steps) {
if (!supportedStepTypes.includes(step.type)) continue
const scriptCode = `${mapPrefix}:${step.code}`
const script = this.builder.createScript(sectionId, scriptCode, step.name || step.code)
scriptIds[scriptCode] = script.id
validSteps.push(step)
}
В результате у нас есть:
полный список скриптов
их ID
steps, которые мы точно умеем обработать
3.2.4. Фаза 2 — Наполнение скриптов командами
Теперь, когда все скрипты созданы — начинаем обрабатывать каждый шаг.
Главные переменные:
const nextScriptCode = step.next_step ? `${mapPrefix}:${step.next_step}` : false
let addDefaultExit = true
let loggerScriptCode = false
1. Работа с аналитикой (log_way_steps)
Если у шага есть log_way_steps, мы создаём отдельный логгер-скрипт:
if (Array.isArray(step.log_way_steps)) {
loggerScriptCode = `${scriptCode}_logger`
const loggerScript = this.builder.createScript(sectionId, loggerScriptCode, `${step.code}_logger`)
Затем в него добавляется JS-команда:
const js = this._buildLoggerContent(logStep)
this.builder.createCommand(loggerScriptCode, 'run_javascript', js)
Почему логгер вынесен в отдельный скрипт?
чтобы шаг не раздувать
чтобы возможные ошибки в основной команде не мешали зафиксировать факт достижения шага
чтобы легко реиспользовать логику логирования (у нас может быть несколько переходов из узла, например, кнопки)
2. switch-case по типам команд
Ключевой блок:
switch(step.type) {
case 'send_text':
case 'entry_point':
case 'log_action':
case 'run_custom_script':
case 'call_llm':
case 'search_knowledgebase':
...
}
Разберём каждый.
send_text
this.builder.createCommand(
scriptCode,
'send_text',
step.content,
step.code,
0
)
Если есть кнопки — мы НЕ создаём дефолтный выход:
if (step.buttons.length > 0) addDefaultExit = false
Кнопки в этот момент НЕ создаются. Они добавляются позже (см. Phase 3).
entry_point
Пока заглушка, но сюда можно добавить:
генерацию deep links
автогенерацию триггеров
multi-entry routing
log_action
Это упрощённая команда аналитики:
this.builder.createCommand(scriptCode, 'run_javascript', this._buildLoggerContent(step))
run_custom_script
Здесь мы вручную генерим JS, который переходит в другой скрипт:
const js = this._buildCustomScriptJs(step)
this.builder.createCommand(scriptCode, 'run_javascript', js)
Почему не используем нативную команду run_sentence?
Потому что она привязана к зависимостям БД, и при удалении секций может нарушать целостность. JavaScript не проверяет foreign keys — это гибче.
call_llm
Здесь начинается генерация JS-кода для LLM-вызова:
this.builder.createCommand(
scriptCode,
'run_js_callback',
this._buildCallLLMJs(step, mapPrefix)
)
Mapper анализирует:
system_prompts (start/final)
provider/model
prompt_table
error_step / next_step
history
response formatting
И превращает всё это в JS-«рецепт», который выполняется в Metabot.
search_knowledgebase
Похожий алгоритм:
this.builder.createCommand(
scriptCode,
'run_js_callback',
this._buildSearchKbJs(step, mapPrefix)
)
Здесь генерится код:
вызов поиска через KnowbaseSearch
сохранение результата в lead.attr
опциональный Telegram-debug
переход в success/not_found/error шаги
3.2.5. Фаза 3 — Default Exit и Transition Collection
После switch-блока мы добавляем переходы:
Логика:
Если есть логгер — первым переходом идём в логгер.
Потом логгер → next_step
Если логгера нет — просто script → next_step
Но если есть кнопки — дефолтный выход НЕ создаётся.
Mapper пока не создаёт команду перехода — только сохраняет их:
transitions.push({ from: scriptCode, to: nextScriptCode })
Это тоже важно: мы не можем создавать переходы, пока не созданы ВСЕ скрипты.
3.2.6. Фаза 4 — Обработка кнопок и создание handler-скриптов
Блок обработки кнопок — наиболее сложный и многослойный участок маппера.
Причина простая: каждая кнопка — это мини-сценарий внутри сценария, и внутренняя логика кнопки может требовать построения отдельных скриптов:
1) Скрипт-обработчик кнопки (handler script)
Создаётся, если:
нужно выставить значение в lead/bot
нужно добавить теги
нужно удалить теги
есть кастомная логика кнопки
2) Скрипт логирования (logger script)
Подключается только если в шаге, к которому относится кнопка, включена аналитика.
3) Целевой скрипт (next_step)
Это обычный переход — либо от самой кнопки (button.next_step), либо от шага (step.next_step).
Таким образом, нажатие кнопки может порождать цепочку:
Handler Script → Logger Script → Next Step
Или:
Logger Script → Next Step
Или:
Handler Script → Next Step
Или просто:
Next Step
Все четыре варианта должны корректно работать. Отсюда и вся сложность логики.
3.2.7. Фаза 5 — Финальное объединение скриптов (TransitionMap)
Теперь, когда:
все скрипты созданы
команды добавлены
кнопки созданы
handler-скрипты созданы
— наконец можно создавать команды-переходы run_sentence — рёбра между объектами.
Mapper строит карту:
for (const { from, to } of transitionMap.values()) {
const toScriptId = scriptIds[to]
this.builder.createCommand(from, 'run_sentence', String(toScriptId), null, 777)
}
Почему sort_order = 777?
Чтобы команда перехода гарантированно была последней.
3.2.8. Генераторы JS-кода (LLM, KB, CustomScript, Logger)
Mapper содержит важные приватные функции:
_buildLoggerContent(step)
_buildCustomScriptJs(step)
_buildCallLLMJs(step)
_buildSearchKbJs(step)
Они генерируют JavaScript прямо на лету, подставляя параметры шага.
Это архитектурно мощно, потому что:
позволяет описывать сложные команды чистым JSON
Mapper превращает JSON → JS → low-code конструкцию
можно легко добавлять новые типы шагов
Однако, лучше это вынести в отдельный модуль или Snippet, чтобы код маппера был чище.
3.2.9. Итог импорта
Mapper возвращает:
return {
success: true,
section_id: sectionId,
created_scripts: createdScriptsCnt
}
3.2.10. Архитектурные преимущества Mapper
Чистая модель автомата состояний
Поддержка аналитики на уровне схемы
Универсальные handler-скрипты для кнопок
Отсутствие foreign-key зависимостей
JS-код, генерируемый на лету
Изоляция каждой воронки через namespace кодов
Предсказуемая однопроходная структура импорта
3.2.11 — Псевдокод полного алгоритма импорта
function runImport(config) {
validate(config)
setBot(config.bot_id)
prefix = `${format}:${version}:${mapCode}`
sectionTitle = `${prefix}:${title}`
deleteOldSection(mapCode)
sectionId = createSection(sectionTitle)
scriptIds = {}
validSteps = []
// ------------------------
// PHASE 1 — Create scripts
// ------------------------
for step in config.steps:
if (!supported(step.type)) continue
scriptCode = `${prefix}:${step.code}`
scriptId = createScript(sectionId, scriptCode)
scriptIds[scriptCode] = scriptId
validSteps.push(step)
transitions = []
menuButtons = []
// ------------------------
// PHASE 2 — Fill scripts
// ------------------------
for step in validSteps:
scriptCode = prefix + ":" + step.code
nextScript = prefix + ":" + step.next_step
if step.log_way_steps:
loggerCode = createLoggerScript()
transitions.push( scriptCode → loggerCode )
switch(step.type):
case send_text:
addSendText(scriptCode)
if (step.buttons) menuButtons.push(...)
else transitions.push(scriptCode → nextScript)
case run_custom_script:
addJS(scriptCode, buildCustomJS(step))
transitions.push(scriptCode → nextScript)
case call_llm:
addJS(scriptCode, buildLLM(step))
transitions.push(scriptCode → nextScript)
case search_knowledgebase:
addJS(scriptCode, buildKB(step))
transitions.push(scriptCode → nextScript)
case log_action:
addJS(scriptCode, buildLogger(step))
transitions.push(scriptCode → nextScript)
// -------------------------
// PHASE 3 — Buttons logic
// -------------------------
for btn in menuButtons:
if btn.hasHandler:
handlerCode = createHandlerScript()
addTagLogic(handlerCode)
addAttrLogic(handlerCode)
addTransition(handlerCode → logger → next)
createMenuItem(scriptId, handlerCode)
else:
jumpScript = resolveJump()
createMenuItem(scriptId, jumpScript)
// -------------------------
// PHASE 4 — Final transitions
// -------------------------
mergeDuplicates(transitions)
for t in transitions:
addCommand(t.from, "run_sentence", scriptIds[t.to])
return success(sectionId)
}














