@retensy/mcp 0.20.0 → 0.21.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,134 +1,134 @@
1
- # Правила валидатора (GraphValidator) — чтобы граф публиковался
2
-
3
- Источник истины: `retensyBackend/.../service/bot/GraphValidator.java`. При `publish` и при PUT **активного** графа бэкенд отвечает `HTTP 422` с `errors: [{ nodeId, code, message }]` — MCP показывает их все построчно (`code@nodeId: message`). Черновик PUT сохраняет без проверок — кроме размера: граф больше 4 МБ не сохраняется ни черновиком, ни активным (`GRAPH_TOO_LARGE`). Ниже — что проверяется и как не нарваться.
4
-
5
- ## Коды ошибок и условия
6
-
7
- ### `GRAPH_TOO_LARGE` — «Сценарий больше 4 МБ»
8
- Граф хранится одним документом. Если JSON `{nodes, edges, canvasMeta}` в UTF-8 длиннее 4 194 304 байт, `update_graph`,
9
- `edit_graph_live`, `patch_graph` и `import_funnel` получают `HTTP 422 GRAPH_TOO_LARGE`, граф не сохранён — **и у черновика
10
- тоже**. Сократи тексты или раздели сценарий на несколько. `validate.mjs` это проверяет.
11
-
12
- ### `SEND_NO_TEXT` — «Пустое сообщение — добавьте текст или картинку»
13
- `SEND_MESSAGE` считается пустым, если **`config.text` пустой/из пробелов И `config.photoUrl` пустой**.
14
- - Бэкенд читает плоское `config.text` (НЕ `cards`).
15
- - Но редактор при сохранении пересобирает `text` из карточек через `cardsToLegacy`: берёт **первую карточку `type:"text"` с непустым `text`** (или `image.url`→`photoUrl`).
16
- - ⇒ ВСЕГДА заполняй и `config.text`, и `cards[0]` (тип `text`, непустой `text`). Тогда оба пути дают непустой текст.
17
- - Кнопок недостаточно: сообщение только с `buttons` — пустое.
18
- - Пробелы/таб/перенос строки = пусто (`.isBlank()`).
19
-
20
- ### Длина текста
21
- - `text` ≤ 4096; если есть `photoUrl` (подпись) — ≤ 1024 (`SEND_TOO_LONG`).
22
- - При `parseMode:"HTML"` — текст должен быть безопасным Telegram-HTML, иначе `HTML_NOT_SAFE`. Разрешены только `b,strong,i,em,u,ins,s,strike,del,code,pre,a[href],tg-spoiler,br`. Если не уверен — `PLAIN`.
23
-
24
- ### `SEND_MESSAGE` в режиме «Вопрос» (`awaitReply:true`)
25
- - `saveTo` обязателен и матчит `[a-z_][a-z0-9_]{0,63}` (`SEND_BAD_SAVE_TO`).
26
- - При `validator:"REGEX"` — `regex` непустой и компилируется (`SEND_BAD_REGEX`).
27
-
28
- ### Триггеры и достижимость
29
- - Нужен **хотя бы один корневой `TRIGGER_*`** без входящих рёбер.
30
- - Если есть `BROADCAST_FILTER` — он должен быть единственным триггером.
31
- - **Все нелистовые узлы достижимы** от какого-либо триггера. Недостижимый узел = ошибка.
32
-
33
- ### Циклы
34
- - **Синхронных циклов нет.** Цикл допустим только если проходит через `ASK_QUESTION`, `DELAY` или `SCHEDULE` (узлы, которые «ждут»).
35
-
36
- ### Per-node конфиг
37
- - `TRIGGER_COMMAND`: `command` обязателен.
38
- - `TRIGGER_CALLBACK`: `value` обязателен; `matchMode` ∈ {EQUALS, STARTS_WITH}.
39
- - `TRIGGER_TEXT`: `matchMode` ∈ {ANY,EQUALS,CONTAINS,REGEX}; для не-ANY нужен `value`.
40
- - `ASK_QUESTION`: `promptText` обязателен (`ASK_NO_PROMPT`); `saveTo` ∈ `[a-z_][a-z0-9_]{0,63}` (`ASK_BAD_SAVE_TO`); при `validator:"REGEX"` — валидный `regex` (`ASK_BAD_REGEX`).
41
- - `BRANCH`: ≥1 case (`BRANCH_NO_CASES`); вне `abTest` — непустое валидное `expression` (`BRANCH_EMPTY_EXPR`/`BRANCH_BAD_EXPR`) и подключённое ребро `case_<id>` (`BRANCH_CASE_UNCONNECTED`; `disabled:true` — если ветка намеренно пустая). В `abTest:true` рёбра нужны только у первых двух case.
42
- - `CONDITION`: `match` ∈ {ALL, ANY} (`CONDITION_BAD_MATCH`); ≥1 элемент в `conditions` (`CONDITION_EMPTY`). Для `kind:"TAG"` — `value` ∈ `[a-z0-9_-]{1,64}` (`CONDITION_BAD_TAG`); для `kind:"VARIABLE"` — `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`CONDITION_BAD_KEY`). Остальные `kind` (UTM/NAME/EMAIL/PHONE/USERNAME/SUBSCRIBED/LINK_CLICKED/CURRENT_*/DAY_OF_WEEK) бэкенд проверяет в рантайме — некорректный `kind`/`op`, нечисловой `key` у `SUBSCRIBED` или `key` у `LINK_CLICKED` без отслеживаемой кнопки молча дают `false` (выход `no`), публикацию не блокируют. Список `kind`/`op` — в schema.md.
43
- - `CALL_WEBHOOK`: `url` обязателен (`WEBHOOK_NO_URL`), http(s):// (`WEBHOOK_BAD_SCHEME`).
44
- - `DELAY`: `kind` ∈ {FIXED,UNTIL,TOMORROW} (`DELAY_BAD_KIND`). `FIXED` — `durationSec>0` (секунды) ИЛИ `duration>0` (+`unit` ∈ {MINUTES,HOURS,DAYS}), иначе `DELAY_BAD_DURATION`. `TOMORROW` — `time` = `HH:mm` (`DELAY_BAD_TIME`). `UNTIL` — `isoTimestamp`: ISO-8601 или шаблон `{{var.x}}` (`DELAY_BAD_TIMESTAMP`; рантайм читает только его, не `isoDate`+`time`); опц. `offset` вида `-24h`/`-30m`/`+1d` (`DELAY_BAD_OFFSET`) и `timezone` (`DELAY_BAD_TIMEZONE`).
45
- - `SET_VARIABLE`: `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`VAR_BAD_KEY`). `ADD_TAG`/`REMOVE_TAG`: `tag` ∈ `[a-z0-9_-]{1,64}` (`TAG_BAD_NAME`).
46
- - `FORMULA`: `expression` непустой (`FORMULA_NO_EXPRESSION`); `saveTo` ∈ var-формат (`FORMULA_BAD_SAVE_TO`).
47
- - `SCHEDULE`: `isoDate` = `YYYY-MM-DD`, ISO-8601 дата-время или шаблон `{{var.x}}` (`SCHEDULE_BAD_DATE`); `time` = `HH:mm` — обязателен при голой дате (`SCHEDULE_BAD_TIME`); опц. `offset` (`SCHEDULE_BAD_OFFSET`), `timezone` (`SCHEDULE_BAD_TIMEZONE`).
48
- - `TRIGGER_SCHEDULE`: `cron` ровно из 5 полей, не чаще раза в минуту (`SCHEDULE_TRIGGER_BAD_CRON`); `timezone` — известная зона (`SCHEDULE_TRIGGER_BAD_TIMEZONE`).
49
- - `ACTIONS`: непустой `actions[]` (`ACTIONS_EMPTY`); каждый `kind` — из допустимого списка (`ACTION_UNKNOWN_KIND`, список — в schema.md); per-kind: `tag` (`ACTION_BAD_TAG`), `set_field.key` (`ACTION_BAD_KEY`), `url` у `external_request`/`subscriber_webhook` (`ACTION_BAD_URL`), `calendarId` у `booking_slots`/`booking_book` (`ACTION_BOOKING_NO_CALENDAR`) и их `saveTo` (`ACTION_BAD_KEY`). Действия Integration Core (`actions[].kind` из `integration_catalog`, например `meta_capi_event`) бэкенд узнаёт по реестру провайдеров; `validate.mjs` их оффлайн не знает и выдаёт предупреждение, а не ошибку.
50
- - `ACTIONS` с действием `issue_invoice` (счёт на кассу владельца):
51
- - `connectionId` обязателен (`YK_NO_CONNECTION`) — подключение с `provider=YOOKASSA` из `list_integrations`;
52
- - `amount` обязателен (`YK_NO_AMOUNT`), число > 0 или шаблон `{{var.x}}` (`YK_BAD_AMOUNT`);
53
- - `description` обязателен (`YK_NO_DESC`);
54
- - `timeoutMinutes` ∈ [1, 1440] (`YK_BAD_TIMEOUT`);
55
- - **счёт должен быть ПОСЛЕДНИМ действием в блоке** (`INVOICE_NOT_LAST`) и только один (`INVOICE_DUPLICATE`).
56
- Причина: при наличии ветки `paid` блок встаёт на паузу до вебхука кассы, а возобновиться
57
- с середины списка движок не умеет — действия после счёта молча не выполнились бы.
58
- - `AI_REPLY` (`mode` не задан или `"simple"`): `userPromptTemplate` непустой (`AI_NO_PROMPT`); `temperature` ∈ [0.0, 2.0] (`AI_BAD_TEMPERATURE`); нужен `sendToUser:true` ИЛИ `saveTo` (`AI_NO_OUTPUT`).
59
- - `AI_REPLY` (`mode: "agent"`) — отдельный набор проверок, старые (`AI_NO_PROMPT` и т.п.) не применяются:
60
- - `strict` (по умолчанию `true`) без `knowledgeBaseId` → `AGENT_NO_KNOWLEDGE_BASE` (в строгом режиме без базы агент ответит «не знаю» на всё).
61
- - `actions[]`: битая запись (пустой `id` ИЛИ `title`) пропускается целиком, как и в рантайме — на неё не ругаемся. Повтор `id` → `AGENT_DUPLICATE_ACTION_ID`. Нет ребра `action_<id>` → `AGENT_ACTION_NOT_CONNECTED`.
62
- - `systemPrompt` длиннее **30 000** символов → ошибка `AGENT_PROMPT_TOO_LONG` (публикация не пройдёт). Длиннее **10 000** — только предупреждение (мягкая норма: длинная инструкция дороже и хуже держится моделью в середине; жёсткого кода для неё у бэкенда нет — в вебе это красит счётчик символов в инспекторе).
63
- - `extract[]`: `variable` обязателен и матчит `[a-z_][a-z0-9_]{0,63}` (тот же `VAR_RE`), `description` непустой, имя `ai_summary` зарезервировано под сводку агента, повтор имени внутри `extract[]` — тоже ошибка. Любая из этих проблем → `AGENT_BAD_EXTRACT`.
64
- - Ветка `unknown` не подключена (и узел достижим от триггера) → `AGENT_UNKNOWN_NOT_CONNECTED`, приходит уровнем `warnings` (не блокирует публикацию — специально мягкий уровень, спека §10): диалог агента упрётся в тупик там, где нужен человек.
65
- - `PAYMENT_LINK`: `paymentUrl` обязателен (`PAY_NO_URL`), http(s):// или `{{var.x}}` (`PAY_BAD_SCHEME`).
66
- - `YOOKASSA_PAYMENT`: **устаревший узел, в новых сценариях не ставить** — счёт собирается действием
67
- `issue_invoice` внутри `ACTIONS`. Если узел всё же есть: `connectionId` (`YK_NO_CONNECTION`),
68
- `amount` (`YK_NO_AMOUNT`/`YK_BAD_AMOUNT`), `description` (`YK_NO_DESC`).
69
- - `TRIGGER_PAYMENT`: обязательных полей нет. Фильтры `minAmount`/`descriptionContains`
70
- необязательны, но **без них сценарий стартует на КАЖДУЮ оплату в кассе**, включая продажи
71
- мимо бота. Валидатор это не блокирует (бэкенд тоже) — следи сам.
72
- - Метки: `[a-z0-9_-]{1,64}`. Переменные: `[a-z_][a-z0-9_]{0,63}`.
73
-
74
- ## Рёбра
75
- - `sourceNodeId` и `targetNodeId` должны указывать на существующие узлы (нет «висячих»).
76
- - `sourceHandle` должен соответствовать типу узла-источника (см. schema.md).
77
- - **Кнопка-выбор ↔ ребро `btn_N`**: у `CALLBACK`-кнопки, от которой идёт ребро `btn_N`, `value` ДОЛЖЕН быть пустым (`""`). Тогда бот рендерит навигационный `callback_data` `n:<id узла>:<индекс>` и маршрутизирует по `btn_N`. Непустой `value` рендерится как есть, не матчит навигационный формат → нажатие уходит в `NO_MATCH` (кнопка «не работает»). Непустой `value` уместен только для legacy-кнопки под отдельный узел `TRIGGER_CALLBACK` (без ребра `btn_N`).
78
- - id узлов и рёбер — уникальны и **оба должны быть валидными UUID**: Jackson десериализует и `TgNode.id`, и `TgEdge.id`/`sourceNodeId`/`targetNodeId` как `java.util.UUID`. Короткая строка (`"m1"`, `"e1"`) → весь PUT падает с HTTP 400 (generic, без тела).
79
-
80
- ## Жизненный цикл
81
- - Статусы графа: `DRAFT` / `PUBLISHED`. Публикация заменяет активную опубликованную версию.
82
- - Правка активного (`PUBLISHED`) графа применяется к боту сразу и проверяется как публикация (плюс платные блоки и лимит блоков тарифа): ошибки → 422, граф не сохранён. `DRAFT` — черновик: PUT сохраняет его без проверок (кроме размера > 4 МБ → `GRAPH_TOO_LARGE`), на бота он не влияет. `publish_graph` черновика копирует его в отдельный `PUBLISHED`-граф (`publishedGraphId`), черновик остаётся `DRAFT` — живые правки бот-сценария делай по `publishedGraphId` (в `list_graphs` у него `isActive: true`; `list_graphs` показывает только сценарии самого бота); вебхук-сценарий правится по своему id.
83
- - Перед публикацией полезно прогнать `dry_run` (kind `command`/`callback`/`text`) — поймать рантайм-проблемы стартовой ветки. Внешних действий `dry_run` не выполняет (шаги с `skipped: "dry-run"`): ответ ИИ, счёт и запись в CRM проверяются только живым прогоном.
84
-
85
- ## Платформенные правила — Instagram
86
-
87
- Источник: `GraphValidator.platformErrors()` (Java). Коды — жёсткие ошибки, **блокируют публикацию** (`publish_graph` вернёт ошибку `HTTP 422` со всеми `code@nodeId`).
88
-
89
- ### `IG_NODE_UNSUPPORTED` — неподдерживаемый тип узла
90
-
91
- Каждый узел в графе (включая черновые/неподключённые) должен быть из IG-allowlist. Всё остальное → ошибка.
92
-
93
- Allowlist: `TRIGGER_IG_COMMENT`, `TRIGGER_IG_DM`, `TRIGGER_IG_STORY_REPLY`, `TRIGGER_IG_STORY_MENTION`, `SEND_MESSAGE`, `SEND_PHOTO`, `BRANCH`, `CONDITION`, `SET_VARIABLE`, `ADD_TAG`, `REMOVE_TAG`, `FORMULA`, `ASK_QUESTION`, `DELAY`, `END`.
94
-
95
- Не в allowlist: `TRIGGER_COMMAND`, `TRIGGER_CALLBACK`, `TRIGGER_TEXT`, `BROADCAST_FILTER`, `SCHEDULE`, `ACTIONS`, `CALL_WEBHOOK`, `AI_REPLY`, `PAYMENT_LINK` — и любой другой тип.
96
-
97
- > Бэкенд проверяет ВСЕ узлы, включая черновые (неподключённые), так как палитра редактора не даёт ставить неподдерживаемые узлы в IG-боте — поэтому их присутствие означает реальную ошибку (напр., импортированный/скопированный граф).
98
-
99
- ### `IG_DELAY_OVER_24H` — задержка больше 24 часов
100
-
101
- Instagram доставляет сообщения только в течение **24 часов** после последнего входящего от пользователя.
102
-
103
- Правило (зеркало `igDelaySeconds()` в Java):
104
- - `kind != "FIXED"` (т.е. `TOMORROW` или `UNTIL`) → всегда ошибка (заведомо > 24ч).
105
- - `kind == "FIXED"`:
106
- - `durationSec > 86400` → ошибка.
107
- - `duration` (число) + `unit` → пересчёт: `MINUTES` × 60, `HOURS` × 3600, `DAYS` × 86400; если результат > 86400 → ошибка.
108
- - `duration` без `unit` → считается в секундах; если > 86400 → ошибка.
109
- - Отсутствует и `durationSec`, и `duration` (malformed FIXED) → ошибка (не разрешаем молча).
110
-
111
- ### `IG_INPUT_UNSUPPORTED` — неподдерживаемый вид ответа
112
-
113
- У `ASK_QUESTION`, `inputKind` ограничен: `TEXT`, `EMAIL`, `PHONE`, `NUMBER`, `CONTACT`.
114
-
115
- `CONTACT` допустим: кнопки «Поделиться номером» в IG Messaging API нет, поэтому вопрос уходит текстом-инструкцией, а номер принимается набранным вручную (как телефон). Не поддерживается: `LOCATION`, `PHOTO`, `DOCUMENT`.
116
-
117
- `inputKind == null` (не задан) — допустимо (дефолт = текст).
118
-
119
- ### Фильтрация по ключевым словам (`keywords`) — только информация, не ошибка
120
-
121
- `TRIGGER_IG_COMMENT`, `TRIGGER_IG_DM`, `TRIGGER_IG_STORY_REPLY` поддерживают опциональное поле `keywords` в config. Значение — произвольная строка (список через запятую или с новой строки). Бэкенд делает **регистронезависимый contains** (вхождение): срабатывает, если входящий текст содержит хотя бы одно из ключевых слов/фраз (по вхождению, без границ слова). Пустое или отсутствующее `keywords` = совпадение с любым событием. Никаких жёстких кодов ошибок для `keywords` нет — любая строка валидна.
122
-
123
- `TRIGGER_IG_STORY_MENTION` фильтрацию по ключевым словам **не поддерживает**: Instagram не передаёт текст упоминания боту, поэтому `keywords` игнорируется (валидатор выдаёт предупреждение, не ошибку).
124
-
125
- ---
126
-
127
- ## Локальная проверка
128
- `node validate.mjs <import.json>` повторяет ключевые проверки: пустые сообщения с учётом `cardsToLegacy`, висячие рёбра, дубли id, достижимость от триггеров, длину текста, HTML-безопасность (эвристика по тегам), режим «Вопрос» (`awaitReply`→`saveTo`/`regex`), конфиг `DELAY`/`SCHEDULE`/`FORMULA`/`ACTIONS`/`AI_REPLY`(оба режима, включая `AGENT_*`)/`PAYMENT_LINK`/триггеров, условия `CONDITION` (вкл. `LINK_CLICKED` со ссылкой на отслеживаемый шаг), размер графа (≤ 4 МБ). Гонять перед каждой заливкой.
129
-
130
- Для IG-ботов передавать `--platform=INSTAGRAM`:
131
- ```bash
132
- node validate.mjs <import.json> --platform=INSTAGRAM
133
- ```
134
- Добавляет проверки `IG_NODE_UNSUPPORTED`, `IG_DELAY_OVER_24H`, `IG_INPUT_UNSUPPORTED` поверх всех обычных, а также предупреждение (WARNING) для `TRIGGER_IG_STORY_MENTION` с заполненным `keywords` (поле игнорируется).
1
+ # Правила валидатора (GraphValidator) — чтобы граф публиковался
2
+
3
+ Источник истины: `retensyBackend/.../service/bot/GraphValidator.java`. При `publish` и при PUT **активного** графа бэкенд отвечает `HTTP 422` с `errors: [{ nodeId, code, message }]` — MCP показывает их все построчно (`code@nodeId: message`). Черновик PUT сохраняет без проверок — кроме размера: граф больше 4 МБ не сохраняется ни черновиком, ни активным (`GRAPH_TOO_LARGE`). Ниже — что проверяется и как не нарваться.
4
+
5
+ ## Коды ошибок и условия
6
+
7
+ ### `GRAPH_TOO_LARGE` — «Сценарий больше 4 МБ»
8
+ Граф хранится одним документом. Если JSON `{nodes, edges, canvasMeta}` в UTF-8 длиннее 4 194 304 байт, `update_graph`,
9
+ `edit_graph_live`, `patch_graph` и `import_funnel` получают `HTTP 422 GRAPH_TOO_LARGE`, граф не сохранён — **и у черновика
10
+ тоже**. Сократи тексты или раздели сценарий на несколько. `validate.mjs` это проверяет.
11
+
12
+ ### `SEND_NO_TEXT` — «Пустое сообщение — добавьте текст или картинку»
13
+ `SEND_MESSAGE` считается пустым, если **`config.text` пустой/из пробелов И `config.photoUrl` пустой**.
14
+ - Бэкенд читает плоское `config.text` (НЕ `cards`).
15
+ - Но редактор при сохранении пересобирает `text` из карточек через `cardsToLegacy`: берёт **первую карточку `type:"text"` с непустым `text`** (или `image.url`→`photoUrl`).
16
+ - ⇒ ВСЕГДА заполняй и `config.text`, и `cards[0]` (тип `text`, непустой `text`). Тогда оба пути дают непустой текст.
17
+ - Кнопок недостаточно: сообщение только с `buttons` — пустое.
18
+ - Пробелы/таб/перенос строки = пусто (`.isBlank()`).
19
+
20
+ ### Длина текста
21
+ - `text` ≤ 4096; если есть `photoUrl` (подпись) — ≤ 1024 (`SEND_TOO_LONG`).
22
+ - При `parseMode:"HTML"` — текст должен быть безопасным Telegram-HTML, иначе `HTML_NOT_SAFE`. Разрешены только `b,strong,i,em,u,ins,s,strike,del,code,pre,a[href],tg-spoiler,br`. Если не уверен — `PLAIN`.
23
+
24
+ ### `SEND_MESSAGE` в режиме «Вопрос» (`awaitReply:true`)
25
+ - `saveTo` обязателен и матчит `[a-z_][a-z0-9_]{0,63}` (`SEND_BAD_SAVE_TO`).
26
+ - При `validator:"REGEX"` — `regex` непустой и компилируется (`SEND_BAD_REGEX`).
27
+
28
+ ### Триггеры и достижимость
29
+ - Нужен **хотя бы один корневой `TRIGGER_*`** без входящих рёбер.
30
+ - Если есть `BROADCAST_FILTER` — он должен быть единственным триггером.
31
+ - **Все нелистовые узлы достижимы** от какого-либо триггера. Недостижимый узел = ошибка.
32
+
33
+ ### Циклы
34
+ - **Синхронных циклов нет.** Цикл допустим только если проходит через `ASK_QUESTION`, `DELAY` или `SCHEDULE` (узлы, которые «ждут»).
35
+
36
+ ### Per-node конфиг
37
+ - `TRIGGER_COMMAND`: `command` обязателен.
38
+ - `TRIGGER_CALLBACK`: `value` обязателен; `matchMode` ∈ {EQUALS, STARTS_WITH}.
39
+ - `TRIGGER_TEXT`: `matchMode` ∈ {ANY,EQUALS,CONTAINS,REGEX}; для не-ANY нужен `value`.
40
+ - `ASK_QUESTION`: `promptText` обязателен (`ASK_NO_PROMPT`); `saveTo` ∈ `[a-z_][a-z0-9_]{0,63}` (`ASK_BAD_SAVE_TO`); при `validator:"REGEX"` — валидный `regex` (`ASK_BAD_REGEX`).
41
+ - `BRANCH`: ≥1 case (`BRANCH_NO_CASES`); вне `abTest` — непустое валидное `expression` (`BRANCH_EMPTY_EXPR`/`BRANCH_BAD_EXPR`) и подключённое ребро `case_<id>` (`BRANCH_CASE_UNCONNECTED`; `disabled:true` — если ветка намеренно пустая). В `abTest:true` рёбра нужны только у первых двух case.
42
+ - `CONDITION`: `match` ∈ {ALL, ANY} (`CONDITION_BAD_MATCH`); ≥1 элемент в `conditions` (`CONDITION_EMPTY`). Для `kind:"TAG"` — `value` ∈ `[a-z0-9_-]{1,64}` (`CONDITION_BAD_TAG`); для `kind:"VARIABLE"` — `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`CONDITION_BAD_KEY`). Остальные `kind` (UTM/NAME/EMAIL/PHONE/USERNAME/SUBSCRIBED/LINK_CLICKED/CURRENT_*/DAY_OF_WEEK) бэкенд проверяет в рантайме — некорректный `kind`/`op`, нечисловой `key` у `SUBSCRIBED` или `key` у `LINK_CLICKED` без отслеживаемой кнопки молча дают `false` (выход `no`), публикацию не блокируют. Список `kind`/`op` — в schema.md.
43
+ - `CALL_WEBHOOK`: `url` обязателен (`WEBHOOK_NO_URL`), http(s):// (`WEBHOOK_BAD_SCHEME`).
44
+ - `DELAY`: `kind` ∈ {FIXED,UNTIL,TOMORROW} (`DELAY_BAD_KIND`). `FIXED` — `durationSec>0` (секунды) ИЛИ `duration>0` (+`unit` ∈ {MINUTES,HOURS,DAYS}), иначе `DELAY_BAD_DURATION`. `TOMORROW` — `time` = `HH:mm` (`DELAY_BAD_TIME`). `UNTIL` — `isoTimestamp`: ISO-8601 или шаблон `{{var.x}}` (`DELAY_BAD_TIMESTAMP`; рантайм читает только его, не `isoDate`+`time`); опц. `offset` вида `-24h`/`-30m`/`+1d` (`DELAY_BAD_OFFSET`) и `timezone` (`DELAY_BAD_TIMEZONE`).
45
+ - `SET_VARIABLE`: `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`VAR_BAD_KEY`). `ADD_TAG`/`REMOVE_TAG`: `tag` ∈ `[a-z0-9_-]{1,64}` (`TAG_BAD_NAME`).
46
+ - `FORMULA`: `expression` непустой (`FORMULA_NO_EXPRESSION`); `saveTo` ∈ var-формат (`FORMULA_BAD_SAVE_TO`).
47
+ - `SCHEDULE`: `isoDate` = `YYYY-MM-DD`, ISO-8601 дата-время или шаблон `{{var.x}}` (`SCHEDULE_BAD_DATE`); `time` = `HH:mm` — обязателен при голой дате (`SCHEDULE_BAD_TIME`); опц. `offset` (`SCHEDULE_BAD_OFFSET`), `timezone` (`SCHEDULE_BAD_TIMEZONE`).
48
+ - `TRIGGER_SCHEDULE`: `cron` ровно из 5 полей, не чаще раза в минуту (`SCHEDULE_TRIGGER_BAD_CRON`); `timezone` — известная зона (`SCHEDULE_TRIGGER_BAD_TIMEZONE`).
49
+ - `ACTIONS`: непустой `actions[]` (`ACTIONS_EMPTY`); каждый `kind` — из допустимого списка (`ACTION_UNKNOWN_KIND`, список — в schema.md); per-kind: `tag` (`ACTION_BAD_TAG`), `set_field.key` (`ACTION_BAD_KEY`), `url` у `external_request`/`subscriber_webhook` (`ACTION_BAD_URL`), `calendarId` у `booking_slots`/`booking_book` (`ACTION_BOOKING_NO_CALENDAR`) и их `saveTo` (`ACTION_BAD_KEY`). Действия Integration Core (`actions[].kind` из `integration_catalog`, например `meta_capi_event`) бэкенд узнаёт по реестру провайдеров; `validate.mjs` их оффлайн не знает и выдаёт предупреждение, а не ошибку.
50
+ - `ACTIONS` с действием `issue_invoice` (счёт на кассу владельца):
51
+ - `connectionId` обязателен (`YK_NO_CONNECTION`) — подключение с `provider=YOOKASSA` из `list_integrations`;
52
+ - `amount` обязателен (`YK_NO_AMOUNT`), число > 0 или шаблон `{{var.x}}` (`YK_BAD_AMOUNT`);
53
+ - `description` обязателен (`YK_NO_DESC`);
54
+ - `timeoutMinutes` ∈ [1, 1440] (`YK_BAD_TIMEOUT`);
55
+ - **счёт должен быть ПОСЛЕДНИМ действием в блоке** (`INVOICE_NOT_LAST`) и только один (`INVOICE_DUPLICATE`).
56
+ Причина: при наличии ветки `paid` блок встаёт на паузу до вебхука кассы, а возобновиться
57
+ с середины списка движок не умеет — действия после счёта молча не выполнились бы.
58
+ - `AI_REPLY` (`mode` не задан или `"simple"`): `userPromptTemplate` непустой (`AI_NO_PROMPT`); `temperature` ∈ [0.0, 2.0] (`AI_BAD_TEMPERATURE`); нужен `sendToUser:true` ИЛИ `saveTo` (`AI_NO_OUTPUT`).
59
+ - `AI_REPLY` (`mode: "agent"`) — отдельный набор проверок, старые (`AI_NO_PROMPT` и т.п.) не применяются:
60
+ - `strict` (по умолчанию `true`) без `knowledgeBaseId` → `AGENT_NO_KNOWLEDGE_BASE` (в строгом режиме без базы агент ответит «не знаю» на всё).
61
+ - `actions[]`: битая запись (пустой `id` ИЛИ `title`) пропускается целиком, как и в рантайме — на неё не ругаемся. Повтор `id` → `AGENT_DUPLICATE_ACTION_ID`. Нет ребра `action_<id>` → `AGENT_ACTION_NOT_CONNECTED`.
62
+ - `systemPrompt` длиннее **30 000** символов → ошибка `AGENT_PROMPT_TOO_LONG` (публикация не пройдёт). Длиннее **10 000** — только предупреждение (мягкая норма: длинная инструкция дороже и хуже держится моделью в середине; жёсткого кода для неё у бэкенда нет — в вебе это красит счётчик символов в инспекторе).
63
+ - `extract[]`: `variable` обязателен и матчит `[a-z_][a-z0-9_]{0,63}` (тот же `VAR_RE`), `description` непустой, имя `ai_summary` зарезервировано под сводку агента, повтор имени внутри `extract[]` — тоже ошибка. Любая из этих проблем → `AGENT_BAD_EXTRACT`.
64
+ - Ветка `unknown` не подключена (и узел достижим от триггера) → `AGENT_UNKNOWN_NOT_CONNECTED`, приходит уровнем `warnings` (не блокирует публикацию — специально мягкий уровень, спека §10): диалог агента упрётся в тупик там, где нужен человек.
65
+ - `PAYMENT_LINK`: `paymentUrl` обязателен (`PAY_NO_URL`), http(s):// или `{{var.x}}` (`PAY_BAD_SCHEME`).
66
+ - `YOOKASSA_PAYMENT`: **устаревший узел, в новых сценариях не ставить** — счёт собирается действием
67
+ `issue_invoice` внутри `ACTIONS`. Если узел всё же есть: `connectionId` (`YK_NO_CONNECTION`),
68
+ `amount` (`YK_NO_AMOUNT`/`YK_BAD_AMOUNT`), `description` (`YK_NO_DESC`).
69
+ - `TRIGGER_PAYMENT`: обязательных полей нет. Фильтры `minAmount`/`descriptionContains`
70
+ необязательны, но **без них сценарий стартует на КАЖДУЮ оплату в кассе**, включая продажи
71
+ мимо бота. Валидатор это не блокирует (бэкенд тоже) — следи сам.
72
+ - Метки: `[a-z0-9_-]{1,64}`. Переменные: `[a-z_][a-z0-9_]{0,63}`.
73
+
74
+ ## Рёбра
75
+ - `sourceNodeId` и `targetNodeId` должны указывать на существующие узлы (нет «висячих»).
76
+ - `sourceHandle` должен соответствовать типу узла-источника (см. schema.md).
77
+ - **Кнопка-выбор ↔ ребро `btn_N`**: у `CALLBACK`-кнопки, от которой идёт ребро `btn_N`, `value` ДОЛЖЕН быть пустым (`""`). Тогда бот рендерит навигационный `callback_data` `n:<id узла>:<индекс>` и маршрутизирует по `btn_N`. Непустой `value` рендерится как есть, не матчит навигационный формат → нажатие уходит в `NO_MATCH` (кнопка «не работает»). Непустой `value` уместен только для legacy-кнопки под отдельный узел `TRIGGER_CALLBACK` (без ребра `btn_N`).
78
+ - id узлов и рёбер — уникальны и **оба должны быть валидными UUID**: Jackson десериализует и `TgNode.id`, и `TgEdge.id`/`sourceNodeId`/`targetNodeId` как `java.util.UUID`. Короткая строка (`"m1"`, `"e1"`) → весь PUT падает с HTTP 400 (generic, без тела).
79
+
80
+ ## Жизненный цикл
81
+ - Статусы графа: `DRAFT` / `PUBLISHED`. Публикация заменяет активную опубликованную версию.
82
+ - Правка активного (`PUBLISHED`) графа применяется к боту сразу и проверяется как публикация (плюс платные блоки и лимит блоков тарифа): ошибки → 422, граф не сохранён. `DRAFT` — черновик: PUT сохраняет его без проверок (кроме размера > 4 МБ → `GRAPH_TOO_LARGE`), на бота он не влияет. `publish_graph` черновика копирует его в отдельный `PUBLISHED`-граф (`publishedGraphId`), черновик остаётся `DRAFT` — живые правки бот-сценария делай по `publishedGraphId` (в `list_graphs` у него `isActive: true`; `list_graphs` показывает только сценарии самого бота); вебхук-сценарий правится по своему id.
83
+ - Перед публикацией полезно прогнать `dry_run` (kind `command`/`callback`/`text`) — поймать рантайм-проблемы стартовой ветки. Внешних действий `dry_run` не выполняет (шаги с `skipped: "dry-run"`): ответ ИИ, счёт и запись в CRM проверяются только живым прогоном.
84
+
85
+ ## Платформенные правила — Instagram
86
+
87
+ Источник: `GraphValidator.platformErrors()` (Java). Коды — жёсткие ошибки, **блокируют публикацию** (`publish_graph` вернёт ошибку `HTTP 422` со всеми `code@nodeId`).
88
+
89
+ ### `IG_NODE_UNSUPPORTED` — неподдерживаемый тип узла
90
+
91
+ Каждый узел в графе (включая черновые/неподключённые) должен быть из IG-allowlist. Всё остальное → ошибка.
92
+
93
+ Allowlist: `TRIGGER_IG_COMMENT`, `TRIGGER_IG_DM`, `TRIGGER_IG_STORY_REPLY`, `TRIGGER_IG_STORY_MENTION`, `SEND_MESSAGE`, `SEND_PHOTO`, `BRANCH`, `CONDITION`, `SET_VARIABLE`, `ADD_TAG`, `REMOVE_TAG`, `FORMULA`, `ASK_QUESTION`, `DELAY`, `END`.
94
+
95
+ Не в allowlist: `TRIGGER_COMMAND`, `TRIGGER_CALLBACK`, `TRIGGER_TEXT`, `BROADCAST_FILTER`, `SCHEDULE`, `ACTIONS`, `CALL_WEBHOOK`, `AI_REPLY`, `PAYMENT_LINK` — и любой другой тип.
96
+
97
+ > Бэкенд проверяет ВСЕ узлы, включая черновые (неподключённые), так как палитра редактора не даёт ставить неподдерживаемые узлы в IG-боте — поэтому их присутствие означает реальную ошибку (напр., импортированный/скопированный граф).
98
+
99
+ ### `IG_DELAY_OVER_24H` — задержка больше 24 часов
100
+
101
+ Instagram доставляет сообщения только в течение **24 часов** после последнего входящего от пользователя.
102
+
103
+ Правило (зеркало `igDelaySeconds()` в Java):
104
+ - `kind != "FIXED"` (т.е. `TOMORROW` или `UNTIL`) → всегда ошибка (заведомо > 24ч).
105
+ - `kind == "FIXED"`:
106
+ - `durationSec > 86400` → ошибка.
107
+ - `duration` (число) + `unit` → пересчёт: `MINUTES` × 60, `HOURS` × 3600, `DAYS` × 86400; если результат > 86400 → ошибка.
108
+ - `duration` без `unit` → считается в секундах; если > 86400 → ошибка.
109
+ - Отсутствует и `durationSec`, и `duration` (malformed FIXED) → ошибка (не разрешаем молча).
110
+
111
+ ### `IG_INPUT_UNSUPPORTED` — неподдерживаемый вид ответа
112
+
113
+ У `ASK_QUESTION`, `inputKind` ограничен: `TEXT`, `EMAIL`, `PHONE`, `NUMBER`, `CONTACT`.
114
+
115
+ `CONTACT` допустим: кнопки «Поделиться номером» в IG Messaging API нет, поэтому вопрос уходит текстом-инструкцией, а номер принимается набранным вручную (как телефон). Не поддерживается: `LOCATION`, `PHOTO`, `DOCUMENT`.
116
+
117
+ `inputKind == null` (не задан) — допустимо (дефолт = текст).
118
+
119
+ ### Фильтрация по ключевым словам (`keywords`) — только информация, не ошибка
120
+
121
+ `TRIGGER_IG_COMMENT`, `TRIGGER_IG_DM`, `TRIGGER_IG_STORY_REPLY` поддерживают опциональное поле `keywords` в config. Значение — произвольная строка (список через запятую или с новой строки). Бэкенд делает **регистронезависимый contains** (вхождение): срабатывает, если входящий текст содержит хотя бы одно из ключевых слов/фраз (по вхождению, без границ слова). Пустое или отсутствующее `keywords` = совпадение с любым событием. Никаких жёстких кодов ошибок для `keywords` нет — любая строка валидна.
122
+
123
+ `TRIGGER_IG_STORY_MENTION` фильтрацию по ключевым словам **не поддерживает**: Instagram не передаёт текст упоминания боту, поэтому `keywords` игнорируется (валидатор выдаёт предупреждение, не ошибку).
124
+
125
+ ---
126
+
127
+ ## Локальная проверка
128
+ `node validate.mjs <import.json>` повторяет ключевые проверки: пустые сообщения с учётом `cardsToLegacy`, висячие рёбра, дубли id, достижимость от триггеров, длину текста, HTML-безопасность (эвристика по тегам), режим «Вопрос» (`awaitReply`→`saveTo`/`regex`), конфиг `DELAY`/`SCHEDULE`/`FORMULA`/`ACTIONS`/`AI_REPLY`(оба режима, включая `AGENT_*`)/`PAYMENT_LINK`/триггеров, условия `CONDITION` (вкл. `LINK_CLICKED` со ссылкой на отслеживаемый шаг), размер графа (≤ 4 МБ). Гонять перед каждой заливкой.
129
+
130
+ Для IG-ботов передавать `--platform=INSTAGRAM`:
131
+ ```bash
132
+ node validate.mjs <import.json> --platform=INSTAGRAM
133
+ ```
134
+ Добавляет проверки `IG_NODE_UNSUPPORTED`, `IG_DELAY_OVER_24H`, `IG_INPUT_UNSUPPORTED` поверх всех обычных, а также предупреждение (WARNING) для `TRIGGER_IG_STORY_MENTION` с заполненным `keywords` (поле игнорируется).