@retensy/mcp 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "retensy-mcp",
3
3
  "displayName": "Retensy MCP",
4
- "version": "0.11.0",
4
+ "version": "0.12.1",
5
5
  "description": "MCP-сервер + скилл для Retensy Bots: сборка и публикация воронок ботов (Telegram/MAX/Instagram) и публикация статей блога в Markdown — всё одним токеном zmcp_.",
6
6
  "author": { "name": "retensy", "url": "https://bots.retensy.com" },
7
7
  "homepage": "https://bots.retensy.com/bots",
package/README.md CHANGED
@@ -19,7 +19,7 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
19
19
  |---|---|---|---|
20
20
  | **Telegram** | Токен бота (BotFather) | `/start`, команды, callback, текст, рассылки | Полный функционал |
21
21
  | **MAX** | Токен бота (MAX Developer) | Команды, callback, текст | Без SUBSCRIBED/reply-клавиатур (мягкие предупреждения) |
22
- | **Instagram** | OAuth в `/growth` (без токена) | Комментарий/Direct/Ответ на историю/Упоминание | Ограниченный набор узлов; DELAY ≤ 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER/CONTACT (CONTACT = ручной ввод номера); без рассылок |
22
+ | **Instagram** | OAuth в `/bots/instagram` (без токена) | Комментарий/Direct/Ответ на историю/Упоминание | Ограниченный набор узлов; DELAY ≤ 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER/CONTACT (CONTACT = ручной ввод номера); без рассылок |
23
23
 
24
24
  ---
25
25
 
@@ -43,7 +43,7 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
43
43
  "mcpServers": {
44
44
  "retensy-mcp": {
45
45
  "command": "npx",
46
- "args": ["-y", "@retensy/mcp"],
46
+ "args": ["-y", "@retensy/mcp@latest"],
47
47
  "env": {
48
48
  "RETENSY_BASE_URL": "https://bots.retensy.com",
49
49
  "RETENSY_MCP_TOKEN": "zmcp_ваш_токен"
@@ -94,6 +94,7 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
94
94
  | `list_bots` | список ботов |
95
95
  | `list_graphs(botId)` | графы (сценарии) бота |
96
96
  | `list_channels(botId)` | каналы/группы, подключённые к боту (chatId для условия SUBSCRIBED) |
97
+ | `list_integrations()` | подключённые сервисы (amoCRM, Битрикс24, GetCourse, Я.Метрика): `id` = `connectionId` для действий сценария |
97
98
  | `get_graph(graphId, [summary], [saveToFile])` | получить граф; `summary:true` — компактная сводка (id/type/title + рёбра), `saveToFile` — записать полный JSON на диск (для больших графов, чтобы не упереться в лимит токенов) |
98
99
  | `create_graph(botId, name)` | создать пустой граф (DRAFT) |
99
100
  | `update_graph(graphId, graphFile\|graph\|nodes,edges)` | залить узлы/рёбра (PUT); `graphFile` — путь к локальному JSON, граф не нужно слать инлайном |
@@ -150,6 +151,54 @@ RETENSY_MCP_TOKEN=zmcp_... node src/index.mjs # стартует stdio MCP-с
150
151
 
151
152
  Зависимостей нет — это голый JSON-RPC по stdio (протокол MCP `2024-11-05`).
152
153
 
154
+ ## Обновления
155
+
156
+ При старте сервер сверяет свою версию с npm (результат кэшируется на 6 часов в
157
+ `~/.retensy-bot-graph/update-check.json`). Если вышла новая — уведомление появится в `setup` и
158
+ в первом ответе инструмента.
159
+
160
+ - **Установка через npx** (вариант B): держите в конфиге `@retensy/mcp@latest` — тогда свежая
161
+ версия подтягивается при запуске. Если пакет установлен глобально, сервер сам запустит
162
+ `npm i -g @retensy/mcp@latest` в фоне (отключается `RETENSY_MCP_AUTOUPDATE=0`).
163
+ - **Установка как плагин** (вариант A): обновляйте плагин/`git pull` — npm тут не при чём,
164
+ исполняется файл репозитория.
165
+
166
+ Важно: запущенный процесс не может подменить собственный код — **обновление вступает в силу после
167
+ перезапуска MCP-сервера**. Нет сети или npm недоступен — проверка молча пропускается, работа не ломается.
168
+
169
+ ## Отчёты о неудачах (телеметрия)
170
+
171
+ Чтобы мы узнавали, каких возможностей не хватает, при неудаче инструмента отправляется **анонимный**
172
+ отчёт: неизвестный инструмент, отказ публикации (`errors[]`), ошибка API.
173
+
174
+ Отчёт уходит на **`POST {RETENSY_BASE_URL}/api/mcp/report`** — то есть на тот же сервер, с которым
175
+ вы и так работаете. Адреса чата/вебхука, куда мы складываем отчёты, в пакете нет: он живёт в
176
+ переменной окружения на сервере. Так его нельзя вытащить из пакета и залить, а мы можем сменить
177
+ приёмник без выпуска новой версии.
178
+
179
+ **Что уходит:** имя инструмента, категория неудачи, текст ошибки, ключи аргументов, версия, платформа
180
+ и анонимный id установки (хэш от имени хоста и домашнего каталога — не сами значения).
181
+ **Что НЕ уходит никогда:** токены, cookie, пароли, креды интеграций, содержимое графов и текстов
182
+ рассылок. Значения аргументов по умолчанию скрыты — присылаются только безопасные поля вроде
183
+ `graphId`/`botId`/`kind`.
184
+
185
+ Если персональный токен настроен, он прикладывается к отчёту — тогда мы видим, у кого именно
186
+ не хватило возможности, и можем ответить. Токен прикладывается **только** когда приёмник совпадает
187
+ с `RETENSY_BASE_URL`: на сторонний `RETENSY_MCP_REPORT_URL` он не отправляется.
188
+
189
+ | Переменная | Значение |
190
+ |---|---|
191
+ | `RETENSY_MCP_TELEMETRY=off` | полностью выключить отчёты |
192
+ | `RETENSY_MCP_TELEMETRY=full` | присылать и значения аргументов (для отладки своей установки) |
193
+ | `RETENSY_MCP_REPORT_URL=<url>` | свой приёмник (напр. локальный сервер), вместо `/api/mcp/report` |
194
+
195
+ Отчёты дедуплицируются и ограничены 20 на запуск, отправка не блокирует ответ инструмента
196
+ (таймаут 4 с) и при сбое сети молча игнорируется. На сервере — свои лимиты (30 в час с адреса,
197
+ 500 в час всего), так что отчёты нельзя использовать для заливки.
198
+
199
+ Отдельно: если токен ещё не настроен вовсе, отчёт **не** отправляется — это обычное состояние
200
+ нового пользователя, а не пробел в возможностях.
201
+
153
202
  ## Безопасность
154
203
 
155
204
  Токен = доступ к аккаунту по API. Не коммить его; держи в `env`. В конфигах храни ссылку `${RETENSY_MCP_TOKEN}`, не само значение.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retensy/mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "MCP server to build and publish Telegram, MAX and Instagram bot funnels/automations — and publish Markdown blog articles — in the Retensy Bots service. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": { "retensy-mcp": "src/index.mjs" },
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "start": "node src/index.mjs",
12
12
  "check": "node --check src/index.mjs",
13
- "test": "node scripts/smoke.mjs",
13
+ "test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs",
14
14
  "validate": "node skills/build-bot-funnel/validate.mjs"
15
15
  },
16
16
  "engines": { "node": ">=18" },
@@ -10,7 +10,7 @@ description: Собрать воронку (сценарий) бота для с
10
10
  **Различия платформ (кратко):**
11
11
  - **Telegram** — полный функционал: `/start` и другие команды, все типы узлов, SUBSCRIBED, кнопки-контакты, рассылки.
12
12
  - **MAX** — те же узлы, но без SUBSCRIBED/reply-клавиатур (мягкие предупреждения при публикации, не блокируют).
13
- - **Instagram** — ограниченный набор узлов; вход только через комментарий/директ/историю (нет `/start`); нет рассылок; DELAY не более 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER/CONTACT (CONTACT = ручной ввод номера, кнопки «Поделиться номером» в IG нет); онбординг через OAuth в разделе «Инструменты роста» (/growth), без токена бота.
13
+ - **Instagram** — ограниченный набор узлов; вход только через комментарий/директ/историю (нет `/start`); нет рассылок; DELAY не более 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER/CONTACT (CONTACT = ручной ввод номера, кнопки «Поделиться номером» в IG нет); онбординг через OAuth на странице `/bots/instagram` («Подключения» → Instagram), без токена бота.
14
14
 
15
15
  ## Когда применять
16
16
  Пользователь описывает сценарий бота словами: приветствие → подписка → вопрос с кнопками → ветки → задержки → вебинар и т.п. Либо просит залить/опубликовать готовую воронку.
@@ -30,7 +30,15 @@ description: Собрать воронку (сценарий) бота для с
30
30
  - `SEND_MESSAGE` умеет быть и сообщением, и **вопросом** (`awaitReply:true` + `saveTo`/`inputKind`/`validator`). У кнопок-ссылок (`kind:"URL"`) есть флаг `track:true` — на такой шаг ссылается условие `LINK_CLICKED`.
31
31
  - **Медиа** (фото/видео/аудио/документ/кружок/галерея): загрузи файл инструментом `upload_file` (локальный `path` или `url` для перезаливки) → получишь публичный URL → вставь его в медиа-карточку `SEND_MESSAGE` (`url`, у `gallery` — `urls[]`) или в `SEND_PHOTO.photoUrl`. Уже загруженное — `list_files`. Бинарь в графе не хранится, только ссылки. Подробности — в schema.md.
32
32
  - `CONDITION` умеет: теги, переменные, UTM-метки, имя/email/телефон из профиля, @username, подписку на канал (`SUBSCRIBED`), клик по ссылке шага (`LINK_CLICKED`), дату/время/день недели. Полная таблица `kind`/`op` — в schema.md.
33
- - Пакет `ACTIONS` — до 30 действий (метки, профиль, HTTP, уведомления, интеграции GetCourse/amoCRM/Google Sheets/Я.Метрика, модерация группы). Список — в schema.md.
33
+ - Пакет `ACTIONS` — до 30 действий (метки, профиль, HTTP, уведомления, интеграции GetCourse/amoCRM/Битрикс24/Google Sheets/Я.Метрика, приём оплат, модерация группы). Список — в schema.md. **Интеграции РАБОТАЮТ** (это не заглушки), но каждой нужен `connectionId` — возьми его через **`list_integrations`**; без него действие упадёт «не выбрано подключение». Для Google Таблиц вместо `connectionId` нужен `googleEmail` (аккаунт подключается в вебе).
34
+ - `AI_REPLY` («AI-агент») — два режима, поле `mode` (`"simple"`/не задан, либо `"agent"`). Платный узел в обоих режимах (`PREMIUM_NODE_FORBIDDEN` на бесплатном тарифе) и недоступен в Instagram-графах (нет в IG-allowlist). Тратит месячный AI-бюджет тарифа — он в токенах, не в рублях.
35
+ - **`mode` не задан или `"simple"`** — ответ модели по `systemPrompt`/`userPromptTemplate` (обязателен, иначе выход `error`). Исчерпание бюджета — не ошибка: узел шлёт `quotaFallbackText` (если задан) и идёт дальше по `next`, как обычно.
36
+ - **`mode: "agent"`** — поиск по подключённой базе знаний (`knowledgeBaseId`), ветки-действия (`actions[]`, каждому — своё ребро `action_<id>`), извлечение переменных из разговора (`extract[]`, значения — слова клиента, не проверенный факт) и готовая сводка на действии в `{{var.ai_summary}}` без отдельной настройки. Без найденных в базе фрагментов (нет базы, либо релевантность ниже порога) агент в ЛЮБОМ режиме (строгом и нет) уходит в `unknown` — модель даже не вызывается; исключение — узел с непустым `actions[]`: он и без фрагментов зовёт модель, чтобы та могла выбрать действие («запишите меня на завтра» слов из базы не содержит), а её ответ без фрагментов в любом режиме (и в нестрогом тоже) уходит в `unknown`; нестрогий режим лишь разрешает модели дополнять УЖЕ найденные фрагменты общими знаниями (кроме цен/сроков/обещаний), а не отвечать вовсе без базы. **У узла нет выхода `next`** — только `answered`/`action_<id>`/`unknown`/`budget_exhausted`/`error` (`error` — сбой модели/поиска, ИИ не настроен, headless-вебхук или исчерпанный бюджет без ветки `budget_exhausted`). Исчерпание бюджета — своя ветка `budget_exhausted` (если подключена) либо `error`, `quotaFallbackText` отправляется в обоих случаях. Если собираешь агента этим скиллом — обязательно веди себя по этому списку выходов, старый одиночный `AI_REPLY` без `mode` для агента не годится. `systemPrompt`: мягкая норма 10 000 символов (у бэкенда для неё нет отдельного предупреждения, только `validate.mjs` этого скилла warn'ит явно), жёсткий потолок 30 000 (`AGENT_PROMPT_TOO_LONG`, блокирует публикацию) — платформа сама добавляет защиту от инъекций/вытягивания инструкции, запрет обещать несделанное и формат под мессенджер, дублировать это в `systemPrompt` не нужно. Подробности всех полей и кодов ошибок (`AGENT_*`) — в schema.md/validation.md.
37
+ - **Приём оплат** — действие `issue_invoice` внутри `ACTIONS` (НЕ отдельный узел: `YOOKASSA_PAYMENT` устарел). Деньги идут на кассу владельца бота; `connectionId` — подключение `provider=YOOKASSA` из `list_integrations`. Два режима, выбирай по задаче:
38
+ - **дождаться оплаты прямо в диалоге** — протяни от блока ветку `paid` (появятся выходы `paid`/`timeout`); сценарий встанет на паузу до подтверждения кассой;
39
+ - **не ждать** — ветку `paid` не рисуй, блок просто пришлёт счёт и пойдёт по `next`; выдать доступ потом сможет отдельный сценарий с триггером `TRIGGER_PAYMENT`.
40
+ Счёт обязан быть **последним** действием в блоке и только одним (`INVOICE_NOT_LAST`, `INVOICE_DUPLICATE`).
41
+ - **`TRIGGER_PAYMENT` («оплата прошла»)** — если счёт выставлял бот, подписчик определяется САМ (счёт помнит его в metadata платежа): можно сразу слать сообщение и вешать метку, разбирать ответ кассы не нужно. Если оплата пришла мимо бота — прогон headless, получателя задавай через `config.target`. Ставь фильтр `minAmount`/`descriptionContains`, иначе сценарий сработает на любую оплату в кассе.
34
42
 
35
43
  4. **Проверь локально** перед заливкой:
36
44
  ```bash
@@ -47,15 +55,15 @@ description: Собрать воронку (сценарий) бота для с
47
55
  - `create_graph(botId, name)` → получить `graphId`. Либо стартуй с готовой основы: `list_templates` → `create_graph_from_template(botId, templateId, name)`.
48
56
  - `update_graph(graphId, nodes, edges, canvasMeta)` → залить узлы/рёбра.
49
57
  - `dry_run(graphId, kind:"command", value:"start")` → прогнать стартовую ветку, проверить `runStatus`.
50
- - `publish_graph(graphId)` → если вернулись `errors[]`, разобрать по `code`/`nodeId`, починить узлы, обновить, опубликовать снова.
58
+ - `publish_graph(graphId)` → при отказе инструмент вернёт ошибку `HTTP 422` со ВСЕМИ причинами построчно (`code@nodeId: message`) — разобрать, починить узлы, обновить, опубликовать снова.
51
59
  - Управление сценариями: `clone_graph`, `rename_graph`, `set_active_graph` (переключить живой граф), `delete_graph` (активный нельзя — сначала переключи).
52
60
  Если MCP не подключён — отдай готовый `import.json` и подскажи: /bots → граф → **Импорт**.
53
61
 
54
62
  5b. **Правка СУЩЕСТВУЮЩЕГО / живого сценария — по умолчанию `edit_graph_live`, а НЕ clone+publish.**
55
- - Когда пользователь просит «поправь сценарий X» (особенно если он уже открыт в редакторе или опубликован) — правь **ТОТ ЖЕ `graphId`** через **`edit_graph_live(graphId, nodes, edges)`**. Он сам снимает авто-бэкап предыдущего состояния (один rolling-граф «🔙 Авто-бэкап») и делает PUT на месте — **id не меняется**.
63
+ - Когда пользователь просит «поправь сценарий X» (особенно если он уже открыт в редакторе или опубликован) — правь **ТОТ ЖЕ `graphId`** через **`edit_graph_live(graphId, nodes, edges)`**. Он сам снимает авто-бэкап предыдущего состояния (один rolling-граф «🔙 Авто-бэкап») и делает PUT на месте — **id не меняется**. Живой сценарий бота — граф с `isActive: true` в `list_graphs` (статус `PUBLISHED`; после `publish_graph` черновика это `publishedGraphId`, а **не** id черновика) — правь его id, иначе правка ляжет в черновик и до бота не дойдёт.
56
64
  - Почему так: бэкенд при PUT/публикации шлёт `external_update` в WS-комнату → открытые редакторы перечитывают граф **вживую** (юзеру не надо перезаходить). Бот читает активный граф **заново из БД на каждое сообщение** → правка живого PUBLISHED-графа применяется **сразу, без отдельной публикации**.
57
- - `clone_graph`+`publish_graph` каждый раз плодят НОВЫЙ id и переключают активный → юзер вынужден открывать новый граф. Так делай только для крупного рискованного рефактора, где нужна изолированная песочница.
58
- - ⚠️ PUT не валидирует (валидирует только `publish`) → перед `edit_graph_live` живого графа **обязательно** прогони `validate.mjs` + `dry_run`. Откат: опубликовать граф «🔙 Авто-бэкап» (или скопировать его содержимое обратно).
65
+ - `clone_graph`+`publish_graph` НЕ создают новый живой граф и НЕ песочница: публикация копирует клон (как любой черновик) в id ТЕКУЩЕГО опубликованного графа бота и затирает его содержимое без бэкапа — вместе с живыми правками, сделанными после прошлой публикации; `set_active_graph` на прежний id после этого вернёт уже новое содержимое, а открытые редакторы живого графа об этом не узнают. Клон годится, чтобы спокойно собрать крупный рискованный рефактор в черновике, но перед его публикацией сохрани живой граф: `get_graph(graphId, saveToFile)` — откат = залить файл обратно через `edit_graph_live`/`update_graph` с `graphFile`.
66
+ - ⚠️ PUT **активного** (PUBLISHED) графа сервер проверяет как `publish`: валидатор, платные блоки, лимит блоков тарифа, платформа. Ошибка → `HTTP 422` со всеми `code@nodeId`, граф **не сохранён**, бот работает на прежней версии. Черновик (`DRAFT`) PUT сохраняет без проверок, и на бота он не влияет: `publish_graph` черновика копирует его в отдельный `PUBLISHED`-граф (`publishedGraphId`), а сам черновик остаётся `DRAFT`. Всё равно прогоняй `validate.mjs` + `dry_run` заранее. Откат: версии активного не хранятся — опубликовать граф «🔙 Авто-бэкап» (или скопировать его содержимое обратно).
59
67
 
60
68
  6. **Отчитайся**: сколько узлов/веток, какие тексты помечены на проверку, ссылка/ id графа.
61
69
 
@@ -64,6 +72,30 @@ description: Собрать воронку (сценарий) бота для с
64
72
  - **Авторизация MCP** — персональный токен (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, полный доступ). Если инструмент вернул «нет токена» или ошибку доступа — **вызови `setup`**, объясни пользователю шаги, попроси прислать токен и сохрани его через **`set_token`** (применяется сразу, без env/рестарта). Также работают env `RETENSY_MCP_TOKEN` и session-cookie.
65
73
  - Бэкенд читает плоские поля `config.text`/`config.photoUrl`; редактор берёт текст из первой карточки `type:"text"`. Поэтому **всегда заполняй и `text`, и `cards`**.
66
74
 
75
+ ## Если возможности не хватает — она фиксируется автоматически
76
+
77
+ При каждой неудаче инструмента отправляется **анонимный** отчёт на `POST /api/mcp/report` того же
78
+ сервера retensy (неизвестный инструмент, отказ публикации `errors[]`, ошибка API). Так мы узнаём,
79
+ чего людям не хватает, и добавляем это в продукт.
80
+
81
+ Что это значит для тебя как ассистента:
82
+ - **Не выдумывай обходные пути для того, чего нет в schema.md.** Просят узел/триггер/действие,
83
+ которого в схеме нет, — скажи прямо, что этого пока нет, и что запрос уже зарегистрирован
84
+ автоматически. Не подменяй это самодельным костылём молча.
85
+ - **Не повторяй один и тот же падающий вызов по кругу.** Отчёты дедуплицируются, но бессмысленные
86
+ ретраи всё равно тратят время пользователя. Разбери `code`/`nodeId` и исправь причину.
87
+ - Секреты в отчёт не попадают: токены, cookie и креды интеграций вырезаются, значения аргументов
88
+ по умолчанию скрыты. Пользователь может выключить отчёты через `RETENSY_MCP_TELEMETRY=off`.
89
+ - **Нужна помощь человека** — если что-то не работает или вопрос не про сборку сценария, направляй
90
+ пользователя в чат «Поддержка» в кабинете bots.retensy.com (кнопка внизу бокового меню): там
91
+ отвечает менеджер. Внешние контакты поддержки не называй.
92
+
93
+ ## Версия сервера
94
+
95
+ При старте сервер сверяется с npm. Если вышла новая версия — уведомление появится в `setup` и
96
+ в первом ответе инструмента. **Увидел такое уведомление — передай его пользователю**: обновление
97
+ вступает в силу только ПОСЛЕ перезапуска MCP-сервера, сам процесс свой код подменить не может.
98
+
67
99
  ## Файлы скилла
68
100
  - [reference/schema.md](reference/schema.md) — формат графа, типы узлов, конфиги, хэндлы.
69
101
  - [reference/validation.md](reference/validation.md) — правила валидатора бэкенда (коды ошибок) и `cardsToLegacy`.
@@ -36,6 +36,44 @@
36
36
  - `TRIGGER_CALLBACK` — `{ "matchMode": "EQUALS"|"STARTS_WITH", "value": "<callback_data>" }`
37
37
  - `TRIGGER_TEXT` — `{ "matchMode": "ANY"|"EQUALS"|"CONTAINS"|"REGEX", "value": "..." }`
38
38
  - `BROADCAST_FILTER` — режим рассылки (если есть — единственный триггер).
39
+ - `TRIGGER_PAYMENT` — в кассе владельца прошла оплата.
40
+ `{ "minAmount": "1000", "descriptionContains": "курс" }` — оба фильтра необязательны.
41
+ **Ставь хотя бы один фильтр:** без них сценарий будет запускаться на КАЖДУЮ оплату в кассе.
42
+ В контексте: `{{payment.id}}`, `{{payment.amount}}`, `{{payment.status}}`,
43
+ `{{payment.description}}`, `{{payment.currency}}`, плюс весь ответ ЮKassa как `body`.
44
+ Запуск — один раз на платёж: повторные уведомления ЮKassa о той же оплате сценарий не перезапускают.
45
+ - **Счёт выставлял бот** (действие `issue_invoice`) → подписчик определяется САМ: счёт
46
+ помнит его в `metadata` платежа. Прогон идёт в обычной сессии — можно прямо ставить
47
+ «Отправить сообщение» и «Добавить метку», указывать получателя и разбирать ответ кассы
48
+ не нужно. Это штатный способ «оплатил → выдать доступ», когда ждать в сценарии не хочется.
49
+ - **Оплата мимо бота** (ссылка из рассылки, счёт из кабинета ЮKassa) → получателя нет,
50
+ прогон headless: узлы отправки адресуй через `config.target`, либо пиши в CRM и таблицы.
51
+ ⚠️ Если оплату надо дождаться ПРЯМО в диалоге, триггер не нужен — веди ветку `paid`
52
+ от блока `ACTIONS` со счётом.
53
+ - `TRIGGER_TG_EVENT` — любое событие Telegram, кроме обычного сообщения: вступил/вышел из канала,
54
+ реакция, буст, заявка в закрытую группу, ответ в опросе и т.п.
55
+ `{ "event": "<имя поля Update>", "filter": { ... }, "priority": 0 }`.
56
+ Рабочие `event` (бэкенд их размечает, остальные молчат): `message`, `edited_message`,
57
+ `callback_query`, `channel_post`, `edited_channel_post`, `chat_member`, `chat_join_request`,
58
+ `my_chat_member`, `message_reaction`, `message_reaction_count`, `chat_boost`,
59
+ `removed_chat_boost`, `poll`, `poll_answer`, `inline_query`, `chosen_inline_result`,
60
+ `shipping_query`, `pre_checkout_query`.
61
+ **НЕ работают** (Telegram их шлёт, но рантайм не разбирает): `business_*`, `chat_shared`,
62
+ `users_shared`, `write_access_allowed`, `purchased_paid_media` — такой триггер не сработает.
63
+ `filter` (все условия по И, пустое = не задано):
64
+ `{"status":"member"|"left"|"kicked"|…}` — только там, где есть `member.status`
65
+ (`chat_member`, `my_chat_member`); `{"chatId":"-1001234567890"}` — конкретный чат/канал
66
+ (сравнение строкой, id каналов не влезают в int), доступен у 12 событий с `chat`;
67
+ `{"text":"купить","textMode":"contains"|"equals"}` — только `message`/`edited_message`/
68
+ `channel_post`/`edited_channel_post`. У `poll`/`poll_answer`/`inline_query`/
69
+ `chosen_inline_result`/`shipping_query`/`pre_checkout_query` чата нет → фильтров тоже.
70
+ Бот должен быть админом канала/группы, иначе события оттуда не придут.
71
+ - `TRIGGER_ANY_UPDATE` — `{}`, ловит ЛЮБОЙ апдейт. Приоритет самый низкий: срабатывает, только
72
+ если не подошёл ни один конкретный триггер. Удобен как «ничего не понял» / отладка.
73
+ - `TRIGGER_WEBHOOK` — `{}`, точка входа сценария с источником WEBHOOK (не бот). Такие сценарии
74
+ создаются в вебе; у графа бота этот триггер не сработает.
75
+ - ~~`TRIGGER_COMMENT`~~ — **мёртвый тип**: рантайм нигде не выставляет `event="comment"`, сработать
76
+ он не может. Убран из палитры редактора. Комментарии Instagram — `TRIGGER_IG_COMMENT`.
39
77
 
40
78
  #### Instagram (только для IG-ботов)
41
79
  IG-боты не поддерживают команды (`/start`). Вход — через взаимодействие с контентом или директ:
@@ -69,6 +107,19 @@ IG-боты не поддерживают команды (`/start`). Вход
69
107
  ### Логика / ветвление
70
108
  - `CONDITION` — проверка условий, выходы `yes` / `no`. `{ "match":"ALL"|"ANY", "conditions":[ { "kind":"...", "op":"...", "key":"...", "value":"..." } ] }`. `match:"ALL"` — все условия истинны; `"ANY"` — хотя бы одно. Полный список `kind`/`op`/полей — в разделе [«Условия CONDITION»](#условия-condition).
71
109
  - `BRANCH` — `{ "cases":[ {"id":"c1","label":"...","expression":"var.x=='a'"} ], "hasDefault": false, "abTest": false }`. Выходы: `case_<id>` (+ `default`).
110
+ - `SWITCH` — развилка по ЗНАЧЕНИЮ, когда веток больше двух.
111
+ `{ "expression":"{{var.http_status}}", "cases":[ {"id":"v1","value":"200","label":"Успех"},
112
+ {"id":"v2","value":"404","label":"Не найдено"} ] }`. Выходы: `case_<id>` + `default` (есть всегда).
113
+ Выражение рендерится как шаблон (обычно просто `{{var.x}}`) и сравнивается со `value` каждого
114
+ случая **без учёта регистра и пробелов по краям**; первое совпадение выигрывает, иначе `default`.
115
+ Отличия: `CONDITION` — бинарное да/нет, `BRANCH` — случайный выбор (A/B), `SWITCH` —
116
+ детерминированный выбор по значению. Валидатор режет: пустое `expression`
117
+ (`SWITCH_NO_EXPRESSION`), нет случаев (`SWITCH_NO_CASES`), пустое `value` (`SWITCH_EMPTY_VALUE`),
118
+ повтор значения (`SWITCH_DUPLICATE_VALUE`), случай без ребра (`SWITCH_CASE_UNCONNECTED`).
119
+ Ребро на `default` необязательно.
120
+ - `STOP_AND_ERROR` — `{ "message":"CRM не ответила: {{var.http_status}}" }`. Обрывает прогон и
121
+ помечает его в журнале как ошибочный (`FAILED`), шаг — `ok:false` с этим текстом. Выходов нет,
122
+ ничего подписчику не отправляет. Пустой `message` → «Сценарий остановлен с ошибкой».
72
123
  - `ASK_QUESTION` — вопрос со сбором ответа. `{ "promptText":"...","saveTo":"name","inputKind":"TEXT"|"PHOTO"|"DOCUMENT"|"CONTACT"|"LOCATION","validator":"ANY"|"PHONE"|"EMAIL"|"REGEX","regex":"...","retryText":"...","maxAttempts":3 }`. `inputKind` (по умолчанию `TEXT`) — что ждём в ответ (`CONTACT` → телефон: в Telegram показывается кнопка «Поделиться номером», в MAX/Instagram кнопки нет — номер вводится вручную и принимается как телефон на всех платформах; `LOCATION` → `lat,lon`, `PHOTO`/`DOCUMENT` → file_id). Выходы `valid` / `invalid`.
73
124
  - `END` — `{}` (конец ветки). **Не добавляй `END`**: ветка и так завершается на узле без исходящих рёбер; явный «конец сценария» бесполезен и убран из палитры редактора. Тип оставлен лишь для совместимости со старыми графами.
74
125
 
@@ -84,16 +135,198 @@ IG-боты не поддерживают команды (`/start`). Вход
84
135
  - `ACTIONS` — непустой пакет действий `{ "actions":[ { "kind":"...", ...поля } ] }`. Допустимые `kind` (иначе ошибка `ACTION_UNKNOWN_KIND`):
85
136
  - **метки/автоворонки**: `add_tag`, `remove_tag`, `autoflow_add`, `autoflow_remove` — поле `tag` (`[a-z0-9_-]{1,64}`)
86
137
  - **профиль**: `set_field` — `key` (`[a-z_][a-z0-9_]{0,63}`) + `value`; `subscribe`, `unsubscribe`
87
- - **HTTP**: `external_request`, `subscriber_webhook` — `url` (http/https) + `method`/`headersJson`/`bodyTemplate`
138
+ - **HTTP**: `external_request` — **основной способ сходить в чужой API**, те же поля и возможности,
139
+ что у узла `CALL_WEBHOOK`: `url` + `method`/`headersJson`/`bodyTemplate`/`timeoutMs` +
140
+ `saveStatusTo`/`saveBodyTo`/`extract`. Отличие одно: своих выходов у действия нет — сбой уводит
141
+ ВЕСЬ блок `ACTIONS` в его выход `error` (если ребро нарисовано; нет — идём по `next`), а
142
+ ветвиться по коду ответа надо следующим блоком `SWITCH`. **Платное действие** (как `CALL_WEBHOOK`):
143
+ на бесплатном тарифе публикация падает с `PREMIUM_NODE_FORBIDDEN`.
144
+ Ещё есть `subscriber_webhook` — `url` + `method`/`headersJson`/`bodyTemplate`
88
145
  - **уведомления**: `notify` (`text`), `subscriber_email` (`email`,`text`), `agent_chat`
89
146
  - **бот/шаг**: `stop_bot`, `delete_step_message`, `cancel_payment_subscription`
90
147
  - **Google Таблицы (работает)**: `gsheets_send` — дописать строку-заявку в таблицу: `{ "kind":"gsheets_send", "googleEmail":"me@gmail.com", "spreadsheetId":"<id таблицы>", "sheetName":"Лист1", "cells":["{{from.first_name}}","{{var.phone}}","{{var.email}}"] }`. `cells` — значения по порядку (шаблоны), бот дописывает их строкой в конец листа. Google-аккаунт подключается В ВЕБЕ (`/bots` → у действия кнопка «Подключить Google»), НЕ через MCP — у пользователя уже должен быть подключён `googleEmail`. Нужны `googleEmail` + `spreadsheetId` + непустой `cells[]` (иначе `ACTION_GSHEETS_INCOMPLETE`).
91
- - **интеграции (пока заглушки, no-op `integration_not_connected`)**: `getcourse_send`, `getcourse_order`, `amocrm_send`, `amocrm_update`, `yametrika_event`, `gsheets_get`, `gsheets_update`, `gsheets_write_cell`, `gsheets_read_cell`
148
+ Остальные четыре действия с таблицами тоже РАБОТАЮТ и тоже требуют `googleEmail` + `spreadsheetId`:
149
+ `gsheets_get` (`range` → `saveTo`), `gsheets_update` (`range`, `values[]`), `gsheets_write_cell`
150
+ (`cell`, `value`), `gsheets_read_cell` (`cell` → `saveTo`; пустая ячейка не ошибка — переменная станет `""`).
151
+ - **CRM и внешние системы — РАБОТАЮТ** (реальные HTTP-клиенты на бэкенде, не заглушки).
152
+ Всем им нужен **`connectionId`** — id подключения пользователя; без него действие падает
153
+ «не выбрано подключение». **Узнать id: инструмент MCP `list_integrations`** (отдаёт
154
+ `{id, provider, title, hint}`; сами креды не отдаются). Значения полей — шаблоны
155
+ (`{{var.x}}`, `{{from.first_name}}`).
156
+ - `amocrm_send` — создать сделку (+контакт): `connectionId`, `leadName`, опц. `price`,
157
+ `pipelineId`, `statusId` (числа), `contactName`, `phone`, `email`. Если все три контактных
158
+ поля после рендера пусты — сделка уходит без контакта. В переменные кладёт `amo_lead_id`
159
+ и `amo_contact_id` (если amo его вернул).
160
+ - `amocrm_update` — частичное обновление сделки: `connectionId`, `leadId` (обычно
161
+ `{{var.amo_lead_id}}`), плюс те же `leadName`/`price`/`pipelineId`/`statusId`. Пустой
162
+ `leadId` или отсутствие полей для обновления — отказ.
163
+ - `bitrix24_call` — любой REST-метод Битрикс24: `connectionId`, **`b24method`** (именно так,
164
+ не `method` — это имя занято HTTP-методом «Внешнего запроса»), `fields` — список пар
165
+ `{key, value}` (ключ вида `fields[TITLE]` разворачивается во вложенную карту),
166
+ `extract` — список `{path, saveTo}` для JsonPath-извлечения ответа в переменные.
167
+ - `getcourse_send` — добавить/обновить пользователя: `connectionId`, `email`, опц. `userName`,
168
+ `phone`, `groups` (CSV групп), `addfields` (карта доп.полей). Повторная заявка обновляет,
169
+ а не дублирует.
170
+ - `getcourse_order` — создать заказ: те же поля пользователя + `offerCode`, опц. `dealStatus`,
171
+ `dealComment`. В переменные кладёт `gc_deal_id`, если GetCourse его вернул.
172
+ - `yametrika_event` — офлайн-конверсия в Я.Метрику: `connectionId`, `idType`
173
+ (`ClientId`|`Yclid`, по умолчанию `ClientId`), **`idValue`** (обязателен после рендера —
174
+ пустой обрывает действие), опц. `target`, `price`, `currency` (по умолчанию `RUB`),
175
+ `dateTime` (unix-секунды, по умолчанию «сейчас»).
176
+ - **Единственные НЕ интегрированные действия**: `agent_chat` и `cancel_payment_subscription` —
177
+ принимается как no-op с пометкой `integration_not_connected`.
92
178
  - **модерация группы**: `group_unban`, `group_kick`, `group_approve`, `group_decline`
93
179
 
94
180
  ### Внешнее / прочее
95
- - `CALL_WEBHOOK` — `{ "url":"https://...", "method":"POST", "bodyTemplate":"{...}", "timeoutMs":5000 }`. Выходы `ok` / `error`.
96
- - `AI_REPLY`, `PAYMENT_LINK`.
181
+ - `CALL_WEBHOOK` — `{ "url":"https://...", "method":"POST", "headersJson":"{\"X-Key\":\"…\"}",
182
+ "bodyTemplate":"{...}", "timeoutMs":5000, "saveStatusTo":"http_status", "saveBodyTo":"http_body",
183
+ "extract":[{"path":"$.id","saveTo":"crm_id"}] }`. Выходы `ok` (код 2xx) / `error` (код ≥ 400,
184
+ сетевая ошибка, таймаут, отказ SSRF-гарда). Если ребра `error` нет — прогон идёт по `next`.
185
+ - `timeoutMs` — на этот узел; 0/не задан = общий клиент (connect 5 с / read 7 с), иначе значение
186
+ прижимается к диапазону **500…30000 мс**.
187
+ - `saveStatusTo` / `saveBodyTo` — имена переменных для HTTP-кода и тела ответа целиком.
188
+ Пишутся ВСЕГДА, в том числе на `error`: при сетевой ошибке код `0` и пустое тело (чтобы в
189
+ переменной не осталось значение прошлого прогона). Тело длиннее 64 КБ обрезается.
190
+ Это штатный способ разветвиться по коду ответа: `saveStatusTo` → `SWITCH`.
191
+ - `extract` — разбор JSON-тела по JsonPath в переменные (работает только на валидном JSON).
192
+ - `AI_REPLY` — ответ модели. Два режима, переключатель — поле `mode` (`"simple"`/не задано, либо
193
+ `"agent"`). Узел без `mode` работает дословно как режим `simple` (обратная совместимость: в
194
+ проде живут графы без этого поля). **Оба режима платные** (`PREMIUM_NODES`): на бесплатном
195
+ тарифе публикация падает с `PREMIUM_NODE_FORBIDDEN`. Веб-редактор показывает поле
196
+ `quotaFallbackText` в инспекторе для ОБОИХ режимов (без подстановки `{{var.x}}` — уходит клиенту
197
+ как есть).
198
+
199
+ **`mode: "simple"`** (сегодняшнее поведение, без изменений):
200
+ `{ "systemPrompt":"Ты консультант магазина.", "userPromptTemplate":"Вопрос: {{last_text}}",
201
+ "sendToUser": true, "saveTo":"ai_answer", "quotaFallbackText":"Спросите менеджера" }`.
202
+ `userPromptTemplate` обязателен (пустой → выход `error`). `sendToUser:true` — отправить ответ
203
+ подписчику; `saveTo` — положить в переменную. Если месячный AI-бюджет тарифа исчерпан, узел НЕ
204
+ ошибка: отправляется `quotaFallbackText` (если задан) и сценарий идёт дальше по `next`. В этом
205
+ случае `saveTo` НЕ обновляется — переменная хранит значение прошлого прогона (или остаётся
206
+ пустой, если прогона ещё не было). Температура задаётся глобально на сервере — поле
207
+ `temperature` в конфиге рантайм не читает.
208
+
209
+ **`mode: "agent"`** — поиск по базе знаний + ветки-действия + память диалога на этом узле. Температура
210
+ агента своя, серверная (`botbuilder.ai.agent-temperature`, по умолчанию 0.3), `temperature` узла не читается
211
+ (спека `docs/superpowers/specs/2026-09-21-ai-agent-node-design.md`, раздел 10). У узла-агента
212
+ **нет выхода `next`** — вместо него именованные ветки ниже.
213
+ ```json
214
+ { "mode": "agent", "knowledgeBaseId": "<id базы знаний>", "strict": true,
215
+ "systemPrompt": "Ты консультант магазина. Отвечай по базе, вежливо и по делу.",
216
+ "actions": [ { "id": "order", "title": "Оформить заказ", "when": "клиент готов купить" } ],
217
+ "extract": [ { "variable": "phone", "description": "телефон клиента" } ],
218
+ "quotaFallbackText": "Спросите менеджера" }
219
+ ```
220
+ Необязательные числовые/флаговые поля и их дефолты (можно не указывать вовсе):
221
+ `directAnswer` (`false`), `directAnswerThreshold` (`0.88`), `minScore` (`0.35`), `topK` (`5`),
222
+ `maxContextChars` (`6000`), `historyTurns` (`6`).
223
+ - `knowledgeBaseId` — id базы знаний ЭТОГО владельца (иначе рантайм ведёт себя так, будто базы
224
+ нет). `strict` (по умолчанию `true`) — без базы в строгом режиме публикация падает с
225
+ `AGENT_NO_KNOWLEDGE_BASE`.
226
+ - **Без найденных в базе фрагментов агент уходит в `unknown` в ЛЮБОМ режиме** — если хитов нет
227
+ или лучший скор ниже `minScore`, до вызова модели дело не доходит вообще (нестрогий режим
228
+ агента без базы или без совпадений не «отвечает по общим знаниям», он так же уходит в
229
+ `unknown`). **Исключение — узел с непустым `actions[]`:** он и без фрагментов зовёт модель
230
+ (платно), чтобы та могла выбрать действие — «запишите меня на завтра» слов из базы не содержит;
231
+ её ответ без фрагментов в любом режиме (и в нестрогом) уходит в `unknown` — только действие. Узел только с
232
+ `extract[]` выходит рано, как раньше. `strict` решает только то, что делает модель, когда релевантные фрагменты уже
233
+ нашлись: в строгом режиме — отвечает исключительно по ним; без строгого режима — может
234
+ дополнить их общими знаниями модели, но не про цены/сроки/обещания — это всегда только из
235
+ базы и инструкции.
236
+ - `directAnswer` (по умолчанию `false`) — прямой ответ готовой парой вопрос-ответ БЕЗ обращения
237
+ к модели (бесплатно), когда совпадение по базе ≥ `directAnswerThreshold`. Пара ищется по вектору
238
+ своего вопроса, так что порог 0.88 проходят почти дословные формулировки. Работает только для
239
+ документов-пар вопрос-ответ (не для кусков файлов/сайта) — кусок PDF, отданный дословно,
240
+ выглядел бы поломкой.
241
+ - `minScore` — порог релевантности фрагмента для решения «есть о чём отвечать»; ниже — выход
242
+ `unknown`. `topK`/`maxContextChars` — сколько фрагментов брать и лимит символов контекста.
243
+ `historyTurns` — сколько реплик истории диалога передавать модели.
244
+ - `systemPrompt` — инструкция агента. Мягкая норма — **до 10 000 символов**: длиннее модель хуже
245
+ держит середину, и каждый ответ дороже. У бэкенда для этого нет отдельного предупреждения при
246
+ публикации (веб-инспектор просто красит счётчик символов) — только `validate.mjs` (этот
247
+ скилл) предупреждает об этом явно. Жёсткий потолок — **30 000 символов**
248
+ (`AGENT_PROMPT_TOO_LONG`, блокирует публикацию и у бэкенда, и у `validate.mjs`). Платформа
249
+ сама добавляет ПЕРЕД инструкцией служебный блок: защиту от инъекций/вытягивания инструкции
250
+ через реплики клиента, историю и фрагменты базы, запрет писать, что действие уже выполнено
251
+ (это делает сценарий, не модель), запрет обещать несделанное, и формат ответа под конкретный
252
+ мессенджер — **дублировать это в `systemPrompt` не нужно**.
253
+ - `actions[]` — ветки, которые выбирает сама модель по смыслу разговора: `id` (латиница/цифры/`_`),
254
+ `title` (человеческое название — без него запись целиком игнорируется рантаймом и валидатором),
255
+ `when` (опц., подсказка модели, когда применять). Каждому действию — своё ребро
256
+ `action_<id>`; повтор `id` → `AGENT_DUPLICATE_ACTION_ID`, отсутствие ребра →
257
+ `AGENT_ACTION_NOT_CONNECTED`.
258
+ - `extract[]` — переменные, которые агент сам заполняет из разговора: `variable`
259
+ (`[a-z_][a-z0-9_]{0,63}`, не `ai_summary` — оно зарезервировано), `description` (что искать).
260
+ Значение — **слова клиента, а не проверенный факт**. Кривая запись → `AGENT_BAD_EXTRACT`.
261
+ - При уходе в действие (`action_<id>`) агент сам готовит сводку разговора для менеджера —
262
+ без отдельной настройки, кладёт её в `{{var.ai_summary}}` (до 1500 символов, тоже слова
263
+ клиента, не проверенные данные).
264
+ - Выходы: `answered` (ответила модель — по базе, а без строгого режима ещё и общими знаниями),
265
+ `action_<id>` (по числу `actions[]`), `unknown` (не знает ответа — нет фрагментов выше
266
+ `minScore`, ветка не подключена не блокирует публикацию, только предупреждение
267
+ `AGENT_UNKNOWN_NOT_CONNECTED`, но диалог упрётся в тупик), `budget_exhausted` (месячный
268
+ AI-бюджет исчерпан, если ветка подключена), `error` (сбой модели/поиска, ИИ не настроен на
269
+ сервере, headless-вебхук — у агента нет собеседника — или исчерпан бюджет, а ветка
270
+ `budget_exhausted` не подключена). Прямой ответ (`directAnswer`) отправляется и идёт в выход
271
+ `answered`, как обычный ответ модели.
272
+ - **Исчерпание бюджета у агента — НЕ `next`** (такого выхода у узла нет): уходит в
273
+ `budget_exhausted`, если ветка подключена, иначе в `error`. `quotaFallbackText` (без
274
+ подстановки переменных) отправляется в обоих случаях, если задан. Это касается и
275
+ `directAnswer` — бесплатный прямой ответ тоже не сработает при исчерпанном бюджете месяца,
276
+ проверка бюджета идёт раньше поиска по базе.
277
+ - `PAYMENT_LINK` — сообщение с кнопкой-ссылкой на оплату (сам платёж не проводит).
278
+ `{ "paymentUrl":"https://example.com/pay?user={{from.id}}", "description":"Оплатите подписку:",
279
+ "buttonText":"Оплатить" }`.
280
+ - `YOOKASSA_PAYMENT` — узел оплаты. **В новых сценариях его не ставь**: выставление счёта — это
281
+ действие, поэтому оно живёт действием `issue_invoice` внутри `ACTIONS` (см. ниже), а узла больше
282
+ нет в палитре. Тип жив в рантайме только ради графов, где он уже стоит.
283
+ - Действие **`issue_invoice`** внутри `ACTIONS` — настоящий счёт на **кассу владельца бота**
284
+ (деньги идут на его магазин, не на счёт сервиса) и, если нужно, ожидание оплаты.
285
+ `{ "kind":"issue_invoice", "connectionId":"<id подключения ЮKassa>", "amount":"990",
286
+ "description":"Доступ к курсу", "buttonText":"Оплатить", "timeoutMinutes":60 }`.
287
+ - `connectionId` **обязателен** — подключение из реестра с `provider=YOOKASSA`
288
+ (`list_integrations`). Нет его → `YK_NO_CONNECTION`. Касса обязана принадлежать владельцу
289
+ бота, чужой id рантайм отвергает.
290
+ - `amount` обязателен, > 0, можно шаблоном (`{{var.price}}`); `description` обязателен.
291
+ - **Выходы даёт БЛОК `ACTIONS`** (тем же приёмом, что и `error`): при наличии счёта у блока
292
+ появляются `paid` и `timeout`. Ключевое правило: **поведение зависит от того, протянул ли ты
293
+ ветку `paid`.**
294
+ - Ветки `paid` НЕТ → блок просто шлёт счёт и идёт по `next`, ничего не дожидаясь.
295
+ - Ветка `paid` ЕСТЬ → сценарий встаёт на паузу до подтверждения оплаты кассой; не дождался за
296
+ `timeoutMinutes` → уйдёт в `timeout`. Сообщения подписчика до оплаты (другие триггеры, ветки до END,
297
+ вопроса или задержки) ожидание не сбрасывают: `paid`/`timeout` всё равно сработают. Сбрасывает его только
298
+ пауза на ДРУГОМ счёте с веткой `paid` — тогда оплата прежней ссылки сценарий не продолжает.
299
+ - ⚠️ **Счёт обязан быть ПОСЛЕДНИМ действием в блоке** (`INVOICE_NOT_LAST`) и только один
300
+ (`INVOICE_DUPLICATE`). Причина: с веткой `paid` блок встаёт на паузу, а возобновиться с
301
+ середины списка движок не умеет — действия после счёта молча не выполнились бы.
302
+ - `timeoutMinutes` — 1…1440, по умолчанию 60. Имеет смысл только вместе с веткой `paid`.
303
+ - Переменные после шага: `{{var.payment_url}}`, `{{var.payment_id}}`.
304
+ - Повторный проход по счёту: пока попытка ЭТОГО шага жива (не оплачена, таймаут не сработал, другой счёт с
305
+ `paid` её не сменил) — та же ссылка (тот же платёж), даже если между проходами подписчик ушёл в другую
306
+ ветку; после оплаты или таймаута — новый счёт. Оплата, пришедшая после таймаута, сценарий не продолжает.
307
+ - Без ветки `paid` ожидания нет: каждый проход по такому счёту — новый платёж с новой ссылкой.
308
+ - Действие бесплатное (в отличие от `external_request`).
309
+ - ⚠️ Чтобы `paid` вообще срабатывал, владелец должен вписать адрес уведомлений из карточки
310
+ подключения в кабинет ЮKassa (событие `payment.succeeded`). Если этого не сделано, оплата
311
+ пройдёт, а сценарий будет молча ждать до таймаута — это не баг графа.
312
+ - Бот выключен в момент оплаты или проверка платежа не удалась (сбой ЮKassa, устаревший ключ в подключении) →
313
+ ЮKassa повторяет уведомление до суток; `paid` сработает на первом удачном повторе, если сценарий к тому
314
+ времени ещё не ушёл в `timeout`. Не удалось за сутки — сценарий по этой оплате не продолжится.
315
+ - `CALL_WEBHOOK` — тоже платный узел, см. раздел «Внешнее / прочее». **В новых сценариях его не
316
+ ставь**: узла больше нет ни в палитре, ни в меню — внешний запрос собирается действием
317
+ `external_request` внутри `ACTIONS`. Тип живёт в рантайме только ради графов, где он уже стоит.
318
+ - Действие `external_request` внутри `ACTIONS` — **тоже платное**, гейт тот же.
319
+
320
+ ## Лимиты тарифа, которые видит сборщик графов
321
+
322
+ - **Блоков в сценарии.** Публикация падает с `NODE_LIMIT_EXCEEDED` (в тексте — сколько блоков в
323
+ графе и сколько даёт тариф). Ошибка на весь граф, `nodeId` пустой. Проверять нечем заранее:
324
+ число блоков берётся из `nodes[]`, лимит — из тарифа владельца.
325
+ - **Число сценариев.** `create_graph`, `create_graph_from_template`, `clone_graph`, `copy_graph` и
326
+ создание сценария в вебе отдают **HTTP 402** `{error, upgradeUrl}`, когда лимит исчерпан.
327
+ Считаются сценарии, которые завёл человек; снимок публикации место не занимает.
328
+ - **Переменные** (глобальные и на сценарий) тоже лимитированы тарифом — сама подсистема переменных
329
+ ещё не построена, поле лимита в тарифе уже есть.
97
330
 
98
331
  ## Условия CONDITION
99
332
 
@@ -124,7 +357,8 @@ IG-боты не поддерживают команды (`/start`). Вход
124
357
 
125
358
  ## Платформа Instagram
126
359
 
127
- IG-боты подключаются через OAuth в разделе **«Инструменты роста»** (`/growth`) — **без вставки токена вручную**; у IG нет персонального бот-токена. После OAuth бот получает доступ к Messaging API через привязанный Instagram Business/Creator-аккаунт.
360
+ IG-боты подключаются через OAuth на странице **`/bots/instagram`** (раздел «Подключения» → карточка
361
+ Instagram; прежний раздел «Инструменты роста» / `/growth` расформирован и редиректит) — **без вставки токена вручную**; у IG нет персонального бот-токена. После OAuth бот получает доступ к Messaging API через привязанный Instagram Business/Creator-аккаунт.
128
362
 
129
363
  ### Разрешённые типы узлов для IG-ботов
130
364
 
@@ -134,14 +368,15 @@ IG-боты подключаются через OAuth в разделе **«Ин
134
368
  |---|---|
135
369
  | `TRIGGER_IG_COMMENT`, `TRIGGER_IG_DM`, `TRIGGER_IG_STORY_REPLY`, `TRIGGER_IG_STORY_MENTION` | ✅ (триггеры входа) |
136
370
  | `SEND_MESSAGE`, `SEND_PHOTO` | ✅ |
137
- | `BRANCH`, `CONDITION` | ✅ |
371
+ | `BRANCH`, `CONDITION`, `SWITCH`, `STOP_AND_ERROR` | ✅ |
138
372
  | `SET_VARIABLE`, `ADD_TAG`, `REMOVE_TAG`, `FORMULA` | ✅ |
139
373
  | `ASK_QUESTION` | ✅ (с ограничениями — см. ниже) |
140
374
  | `DELAY` | ✅ (не более 24ч — см. ниже) |
141
375
  | `END` | ✅ |
142
376
  | `TRIGGER_COMMAND`, `TRIGGER_CALLBACK`, `TRIGGER_TEXT` | ❌ |
143
377
  | `BROADCAST_FILTER` (рассылки) | ❌ |
144
- | `SCHEDULE`, `ACTIONS`, `CALL_WEBHOOK`, `AI_REPLY`, `PAYMENT_LINK` | ❌ |
378
+ | `SCHEDULE`, `ACTIONS`, `CALL_WEBHOOK`, `AI_REPLY`, `PAYMENT_LINK`, `YOOKASSA_PAYMENT` | ❌ |
379
+ | `TRIGGER_PAYMENT` | ❌ |
145
380
 
146
381
  ### Ограничения IG-ботов
147
382
 
@@ -186,8 +421,13 @@ node validate.mjs graph.json --platform=INSTAGRAM
186
421
  | `ASK_QUESTION` | `valid`, `invalid` |
187
422
  | `SEND_MESSAGE` с `awaitReply:true` | `valid`, `invalid` (+ `btn_N` для кнопок) |
188
423
  | `CALL_WEBHOOK` | `ok`, `error` |
424
+ | `ACTIONS` | `next`, плюс `error` — если внутри есть действие, которое может упасть (внешний запрос, CRM, Таблицы) |
425
+ | `SWITCH` | `case_<id>`, `default` |
426
+ | `STOP_AND_ERROR` | выходов нет (терминатор) |
189
427
  | `SCHEDULE` | `scheduled`, `past` |
190
428
  | `DELAY` | `next` |
429
+ | `AI_REPLY` (`mode` не задан/`simple`) | `next`, `error` |
430
+ | `AI_REPLY` (`mode:"agent"`) | `answered`, `action_<id>` (по числу `actions[]`), `unknown`, `budget_exhausted`, `error` — **выхода `next` нет** |
191
431
 
192
432
  ## Подстановки в тексте
193
433
  `{{from.first_name}}`, `{{from.username}}`, `{{var.<имя>}}`, либо `{Имя}` как плейсхолдер. Имена переменных/меток: `[a-z_][a-z0-9_]{0,63}` (var) и `[a-z0-9_-]{1,64}` (tag).