@retensy/mcp 0.23.0 → 0.24.0

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.
@@ -7,7 +7,7 @@
7
7
  "name": "retensy-mcp",
8
8
  "source": "./",
9
9
  "version": "0.15.1",
10
- "description": "Сборка и публикация воронок ботов (Telegram/MAX, Retensy Bots) из описания, рассылки, подключение ботов и сервисов, сайтов из блоков + публикация статей блога в Markdown — через MCP + скилл."
10
+ "description": "Сборка и публикация воронок ботов (Telegram/MAX, Retensy Bots) из описания или переноса из другого конструктора (BotHelp, SaleBot, Senler и др.), рассылки, подключение ботов и сервисов, сайтов из блоков + публикация статей блога в Markdown — через MCP + скилл."
11
11
  }
12
12
  ]
13
13
  }
@@ -2,7 +2,7 @@
2
2
  "name": "retensy-mcp",
3
3
  "displayName": "Retensy MCP",
4
4
  "version": "0.15.1",
5
- "description": "MCP-сервер + скилл для Retensy Bots: подключение ботов и сервисов, сборка и публикация воронок ботов (Telegram/MAX), рассылки, сайты из блоков (Zero-блок, дизайны, шаблоны, свои домены, заявки) и статьи блога в Markdown — всё одним токеном zmcp_.",
5
+ "description": "MCP-сервер + скилл для Retensy Bots: подключение ботов и сервисов, сборка и публикация воронок ботов (Telegram/MAX), перенос воронок из других конструкторов (BotHelp, SaleBot, Senler, Leadteh, Smartsender, ManyChat и др.), рассылки, сайты из блоков (Zero-блок, дизайны, шаблоны, свои домены, заявки) и статьи блога в Markdown — всё одним токеном zmcp_.",
6
6
  "author": { "name": "retensy", "url": "https://bots.retensy.com" },
7
7
  "homepage": "https://bots.retensy.com/bots",
8
8
  "repository": "https://github.com/retensy/retensy-mcp",
package/README.md CHANGED
@@ -7,6 +7,47 @@
7
7
 
8
8
  MCP-сервер (+ скилл для Claude Code) для **сборки и публикации воронок/автоматизаций ботов (Telegram, MAX и Instagram)** в сервисе [retensy `/bots`](https://bots.retensy.com/bots): из текстового описания → валидный граф сценария → заливка и публикация через API.
9
9
 
10
+ ---
11
+
12
+ ## Быстрый старт
13
+
14
+ 1. **Получи токен.** Залогинься на https://bots.retensy.com → открой `/bots/mcp-tokens` → создай токен → скопируй секрет `zmcp_...` (показывается один раз).
15
+ 2. **Подключи у своего клиента** (env-переменные везде одни и те же: `RETENSY_BASE_URL`, `RETENSY_MCP_TOKEN`):
16
+
17
+ **Claude Desktop** — добавь в `claude_desktop_config.json`:
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "retensy-mcp": {
22
+ "command": "npx",
23
+ "args": ["-y", "@retensy/mcp@latest"],
24
+ "env": {
25
+ "RETENSY_BASE_URL": "https://bots.retensy.com",
26
+ "RETENSY_MCP_TOKEN": "zmcp_ваш_токен"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Claude Code** — одной командой:
34
+ ```bash
35
+ claude mcp add retensy-mcp -e RETENSY_BASE_URL=https://bots.retensy.com -e RETENSY_MCP_TOKEN=zmcp_ваш_токен -- npx -y @retensy/mcp@latest
36
+ ```
37
+ Или как плагин (подтянет и скиллы, см. [«Вариант A» ниже](#вариант-a--как-плагин-claude-code-рекомендуется)):
38
+ ```text
39
+ /plugin marketplace add retensy/retensy-mcp
40
+ /plugin install retensy-mcp@retensy
41
+ ```
42
+
43
+ **Cursor** — Settings → MCP → Add new MCP Server (или `.cursor/mcp.json` в проекте) — тот же JSON-объект, что и для Claude Desktop выше (`mcpServers.retensy-mcp` с `command`/`args`/`env`).
44
+
45
+ 3. **Собери первую воронку.** Опиши сценарий словами агенту — он сам соберёт граф и опубликует (см. [«Использование»](#использование)), либо перенеси готовую воронку из другого сервиса — см. [«Перенос воронки из другого конструктора»](#перенос-воронки-из-другого-конструктора) ниже.
46
+
47
+ Проверить подключение в любом клиенте: спроси агента «настрой подключение» — вызовет `setup` и подскажет, если что-то не так.
48
+
49
+ ---
50
+
10
51
  - 🤖 **Сценарии ботов**: `list_bots`, `list_graphs`, `list_channels`, `get_graph`, `create_graph`, `update_graph`, `edit_graph_live`, `patch_graph`, `dry_run`, `publish_graph`, `import_funnel`, `list_templates`, `create_graph_from_template`, `clone_graph`, `copy_graph`, `rename_graph`, `set_active_graph`, `delete_graph`, `upload_file`, `list_files`, `delete_file`, `graph_analytics`, `list_bot_users`, `list_links` (+ `setup`/`set_token`).
11
52
  - 🔌 **Боты и сервисы**: `create_bot` (Telegram/MAX по токену), `bot_stop`/`bot_resume`, `connect_integration`/`disconnect_integration` (amoCRM, Битрикс24, GetCourse, Я.Метрика, ЮKassa; Google Таблицы — ссылкой на вход Google; креды хранятся зашифрованными и не возвращаются), `integration_catalog`/`integration_status`/`integration_test` (каталог Integration Core и живая проверка подключения), `integration_ingress_url` (URL ингресса «Внешних событий» — Tilda, JivoSite, WooCommerce, amoCRM, Битрикс24, GetCourse, CloudPayments, Robokassa, Т-Банк, Prodamus — для `TRIGGER_WEBHOOK`), `channel_post` (пост в канал Telegram/MAX).
12
53
  - 📣 **Рассылки**: `broadcast_preview` (размер аудитории), `broadcast_send` (сейчас или по расписанию, сразу по нескольким ботам; сообщения или запуск сценария; полный фильтр аудитории `audienceFilter`, дедуп `dedup` и распределённый запуск `stagger` по ботам), `broadcast_list`/`broadcast_get`/`broadcast_cancel`, повторяющиеся (`broadcast_recurring`), черновики (`broadcast_drafts`, `broadcast_duplicate`), мульти-бот группы (`broadcast_group` — preview/list/get/pause/resume/cancel) + скилл `send-broadcast`.
@@ -20,6 +61,7 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
20
61
  - 📎 **Медиа**: `upload_file` грузит фото/видео/документы в библиотеку **/bots/files** (до 50 МБ) и возвращает публичный URL — его вставляешь в медиа-карточку сценария.
21
62
  - 🌐 **Сайты из блоков** (раздел «Страницы»): создание, правка операциями (блоки, Zero-блок со свободной вёрсткой, код блока, папки страниц, дизайны, шаблоны из библиотеки), публикация и откат, свои домены, заявки из форм и куда их доставлять — инструменты `site_*` + скилл `build-site`.
22
63
  - 🧠 **Скилл `build-bot-funnel`**: учит агента собирать корректный граф (типы узлов, ветки, кнопки, задержки) и проверять его перед публикацией. Поддерживает Telegram, MAX и Instagram.
64
+ - 🔁 **Скилл `rebuild-funnel-from-builder`**: переносит воронку из другого конструктора (BotHelp, SaleBot, Senler, Leadteh, Smartsender, ManyChat, PuzzleBot и др.) по export JSON/тексту, скриншотам или пересказу — в граф Retensy, новым черновиком, без перезаписи текущего сценария. См. [«Перенос воронки из другого конструктора»](#перенос-воронки-из-другого-конструктора).
23
65
  - 📦 **Без зависимостей** — чистый Node ≥18, ставится и запускается сразу.
24
66
 
25
67
  ### Поддерживаемые платформы
@@ -41,7 +83,7 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
41
83
  /plugin install retensy-mcp@retensy
42
84
  ```
43
85
 
44
- Подтянутся MCP-сервер и скиллы `build-bot-funnel`, `build-site`, `send-broadcast`. Проверить: `/mcp` и `/plugin`.
86
+ Подтянутся MCP-сервер и скиллы `build-bot-funnel`, `rebuild-funnel-from-builder`, `build-site`, `send-broadcast`. Проверить: `/mcp` и `/plugin`.
45
87
 
46
88
  ### Вариант B — как обычный MCP-сервер (Claude Code / Cursor / Windsurf / любой MCP-клиент)
47
89
 
@@ -79,6 +121,10 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
79
121
 
80
122
  Отозвать токен можно там же — доступ блокируется мгновенно.
81
123
 
124
+ > С 14.10 тот же токен `zmcp_...` полностью покрывает и вебхук-сценарии (`/api/scenarios` — `scenario_*`),
125
+ > и управление подключениями сервисов (`/api/integrations` — `connect_integration`/`disconnect_integration`/
126
+ > `integration_update` и остальные `integration_*`), не только `/api/bots`. Отдельного токена под эти инструменты не нужно.
127
+
82
128
  > **Не знаешь, что делать?** Скажи агенту «настрой подключение» — он вызовет `setup`, объяснит шаги и попросит токен. Любой инструмент при отсутствии токена тоже вернёт пошаговую инструкцию.
83
129
 
84
130
  > Дев-окружение: `RETENSY_BASE_URL=http://localhost:8066`.
@@ -225,6 +271,15 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
225
271
  | `scenario_timeseries(graphId, from, to)` | дневной ряд прогонов сценария `{ok, failed, other}` (по узлам — `graph_analytics`) |
226
272
  | `agent_tool_test(agentId, toolId, params?)` | проверить инструмент агента (`AMO_LOOKUP`/`SHEETS_LOOKUP`/`HTTP_API`) напрямую, без модели — бюджет ИИ не тратится |
227
273
  | `site_lead_file_download(siteId, leadId, fileId)` | метаданные вложения заявки + ссылка на скачивание (сам файл через MCP не передаётся — HEAD-запрос, не GET) |
274
+ | `scenario_list()` | сценарии автора, включая вебхук-сценарии (`sourceType: WEBHOOK`\|`BOT`) — в `list_graphs` вебхук-сценарии не входят |
275
+ | `scenario_create(name, botId?)` | создать сценарий: без `botId` — вебхук-сценарий (сервер сам выпускает путь входа и секрет), с `botId` — бот-сценарий, сразу привязанный к боту |
276
+ | `scenario_get(scenarioId)` | сценарий целиком — граф + `source` (у вебхук-сценария: путь/секрет входа) + `outputBinding` |
277
+ | `scenario_update_source(scenarioId, botId?, channel?, recipientFrom?)` | привязка выхода вебхук-сценария, заменяется ЦЕЛИКОМ (без `botId` — привязка снимается, без `recipientFrom` — очищается); только для `sourceType:"WEBHOOK"` |
278
+ | `scenario_publish(scenarioId)` | опубликовать вебхук-сценарий (бот-сценарий — через `publish_graph`) |
279
+ | `scenario_rotate_secret(scenarioId)` | выпустить новый секрет входа вебхук-сценария — старый сразу перестаёт приниматься |
280
+ | `agent_api_key_create(agentId, name?)` / `agent_api_key_list(agentId)` / `agent_api_key_revoke(agentId, keyId, confirm:true)` | API-ключи публичного чата агента (`zagt_...`, секрет виден один раз) — для `POST /api/public/agents/{agentId}/chat` без основного токена |
281
+ | `ai_packs()` | каталог пакетов ответов ИИ сверх тарифа + текущий баланс (`aiPackAnswersLeft`) |
282
+ | `ai_pack_purchase(sku, method?, confirm:true)` | купить пакет — возвращает ссылку на оплату, деньги не списываются этим вызовом |
228
283
 
229
284
  ### Рассылки
230
285
 
@@ -285,6 +340,20 @@ node skills/build-bot-funnel/validate.mjs path/to/import.json --platform=MAX
285
340
 
286
341
  ---
287
342
 
343
+ ## Перенос воронки из другого конструктора
344
+
345
+ Есть готовая воронка в BotHelp, SaleBot, Senler, Leadteh, Smartsender, ManyChat, PuzzleBot или
346
+ любом другом сервисе — скилл [`rebuild-funnel-from-builder`](skills/rebuild-funnel-from-builder/SKILL.md)
347
+ учит агента перенести её в Retensy: разобрать export JSON/текст/скриншоты/пересказ → построить
348
+ инвентарь блоков → сопоставить с реальными типами узлов Retensy (таблица в
349
+ [reference/mapping.md](skills/rebuild-funnel-from-builder/reference/mapping.md)) → прямо спросить
350
+ про то, что не мапится, вместо того чтобы выдумывать замену → собрать граф → проверить
351
+ `validate.mjs`/`dry_run` → залить **новым** черновиком (никогда не поверх текущего сценария) →
352
+ опубликовать только после явного подтверждения. Перенос самих подписчиков (не графа) — отдельно,
353
+ через `migration_presets`/`migration_preview`/`migration_commit` (CSV).
354
+
355
+ ---
356
+
288
357
  ## Разработка
289
358
 
290
359
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retensy/mcp",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "MCP server to build and publish Telegram and MAX bot funnels/automations, broadcasts, service integrations, block-based websites (incl. free-layout Zero blocks, designs, templates, custom domains, form leads) and Markdown blog articles in the Retensy Bots service. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "start": "node src/index.mjs",
23
23
  "check": "node --check src/index.mjs",
24
- "test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs",
24
+ "test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs",
25
25
  "validate": "node skills/build-bot-funnel/validate.mjs"
26
26
  },
27
27
  "engines": {
@@ -73,8 +73,15 @@
73
73
  Бот должен быть админом канала/группы, иначе события оттуда не придут.
74
74
  - `TRIGGER_ANY_UPDATE` — `{}`, ловит ЛЮБОЙ апдейт. Приоритет самый низкий: срабатывает, только
75
75
  если не подошёл ни один конкретный триггер. Удобен как «ничего не понял» / отладка.
76
- - `TRIGGER_WEBHOOK` — `{}`, точка входа сценария с источником WEBHOOK (не бот). Такие сценарии
77
- создаются в вебе; у графа бота этот триггер не сработает.
76
+ - `TRIGGER_WEBHOOK` — `{}`, точка входа сценария с источником WEBHOOK (не бот); у графа БОТ-сценария
77
+ этот триггер не сработает. Такой сценарий создаётся `scenario_create(name)` без `botId` — сервер сам
78
+ выпускает путь входа и секрет (видны в `scenario_get` → `source.config`, ротируются `scenario_rotate_secret`);
79
+ узлы/рёбра — как у обычного графа, через `update_graph`/`edit_graph_live`/`patch_graph` по вернувшемуся id.
80
+ Выходной бот (кому слать ответ) задаётся/снимается отдельно через `scenario_update_source(scenarioId, botId?,
81
+ channel?, recipientFrom?)` — поле заменяется ЦЕЛИКОМ, без `botId` сценарий остаётся headless. Публикуется
82
+ такой сценарий `scenario_publish`, НЕ `publish_graph` (у него на source.type WEBHOOK — 409). Список своих
83
+ сценариев, включая вебхук-сценарии (их нет в `list_graphs`), — `scenario_list`; удаление — `delete_graph`
84
+ (тем же id). Собрать так же можно и в вебе — оба пути ведут в один и тот же объект.
78
85
  - **Внешние события интеграции** — тот же `TRIGGER_WEBHOOK`, но с заполненными
79
86
  `{ "provider": "<provider-ключ из integration_catalog, нижний регистр>", "connectionId": "<id из list_integrations>",
80
87
  "event": "<events[].key нужного события из inbound.events того же provider в integration_catalog>" }`.
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: rebuild-funnel-from-builder
3
+ description: Перенести воронку (сценарий бота) из другого конструктора — BotHelp, SaleBot, Senler, Leadteh, Smartsender, ManyChat, PuzzleBot или любого другого — в Retensy. На входе export JSON/текст, скриншоты редактора или устное описание воронки другого сервиса; на выходе валидный граф retensy-bot-graph, созданный НОВЫМ черновиком (никогда не перезаписывая существующий активный сценарий), проверенный validate.mjs/dry_run и опубликованный только после подтверждения пользователя. Использовать, когда пользователь просит «перенеси воронку из BotHelp/SaleBot/...», «импортируй бота с другого конструктора», присылает экспорт JSON/CSV другого сервиса, скриншоты его редактора сценариев или пересказывает словами, как была устроена воронка там.
4
+ ---
5
+
6
+ # Перенос воронки из другого конструктора в Retensy
7
+
8
+ Превращает воронку, собранную в ДРУГОМ сервисе (BotHelp, SaleBot, Senler, Leadteh, Smartsender,
9
+ ManyChat, PuzzleBot — или любом не названном здесь), в валидный граф `retensy-bot-graph`. Источник —
10
+ что угодно: export JSON/CSV, скопированный текст из экспорта, скриншоты экрана редактора, либо
11
+ устный пересказ пользователем «как это было устроено».
12
+
13
+ Это не альтернатива [`build-bot-funnel`](../build-bot-funnel/SKILL.md), а его надстройка для
14
+ конкретного случая «source = чужой конструктор»: формат графа, типы узлов, правила валидатора,
15
+ публикация — всё то же самое, описано там. Прочитай
16
+ [`../build-bot-funnel/reference/schema.md`](../build-bot-funnel/reference/schema.md) и
17
+ [`../build-bot-funnel/reference/validation.md`](../build-bot-funnel/reference/validation.md) —
18
+ это единственный источник правды о том, какие типы узлов РЕАЛЬНО существуют в Retensy. Не выдумывай
19
+ несуществующий тип узла только потому, что в другом конструкторе была похожая кнопка.
20
+
21
+ ## Когда применять
22
+
23
+ Пользователь присылает экспорт/скриншоты/описание воронки ДРУГОГО бот-конструктора и просит её
24
+ «перенести», «воссоздать», «импортировать», «сделать то же самое у нас». Если он просто описывает
25
+ воронку своими словами без привязки к другому сервису — это обычный `build-bot-funnel`.
26
+
27
+ ## Рабочий процесс
28
+
29
+ ### 1. Собери вход и опиши его как инвентарь блоков
30
+
31
+ Прими то, что есть, без требования единого формата:
32
+ - **Export JSON/XML/CSV** другого конструктора — прочитай файл целиком, не фрагментом.
33
+ - **Вставленный текст** экспорта (многие сервисы отдают воронку как список шагов текстом/таблицей).
34
+ - **Скриншоты редактора** — опиши по ним каждый блок: тип, текст, кнопки и их подписи, куда ведёт
35
+ каждая кнопка (по стрелке или по названию следующего блока).
36
+ - **Устный пересказ** — задавай уточняющие вопросы по узлам (см. п.3), не додумывай недостающее
37
+ самостоятельно без спроса.
38
+
39
+ Построй **плоский инвентарь блоков** ДО всякого маппинга: порядковый номер/id в исходнике, тип блока
40
+ как он назван у источника (напр. «Сообщение», «Условие», «Задержка», «Webhook», «Оплата»), текст/содержимое,
41
+ исходящие связи (на какой блок/по какому условию). Это промежуточный список, который потом построчно
42
+ переводится в узлы Retensy — не пропускай этот шаг и не иди сразу в генерацию JSON по памяти о скриншотах.
43
+
44
+ ### 2. Сопоставь концепции с типами узлов Retensy
45
+
46
+ Таблица сопоставления — в [reference/mapping.md](reference/mapping.md): по каждому общему понятию
47
+ builders (сообщение, кнопка, задержка, тег, переменная, условие/ветка, оплата, форма, интеграция,
48
+ AI/бот-ответчик, рассылка, вебхук) — какой РЕАЛЬНЫЙ тип узла/конфиг Retensy его покрывает, и ссылка
49
+ на точный раздел schema.md. Особенности конкретных источников (BotHelp, SaleBot, Senler, Leadteh,
50
+ Smartsender, ManyChat, PuzzleBot) и частые ловушки каждого — там же.
51
+
52
+ Общее правило маппинга: сопоставляй **по смыслу шага**, а не по названию блока источника. «Условие»
53
+ в одном конструкторе может оказаться у нас `CONDITION` (да/нет), `BRANCH` (A/B) или `SWITCH`
54
+ (детерминированный выбор по значению) — смотри, что блок реально делает.
55
+
56
+ ### 3. Зафиксируй немаппируемое и спроси пользователя
57
+
58
+ После прохода по инвентарю у тебя будет три корзины:
59
+ - **Маппится напрямую** — идёт в граф.
60
+ - **Маппится с потерей/приближением** (напр. специфичный виджет оплаты источника → наш `issue_invoice`
61
+ без части его настроек) — помечай и предупреждай пользователя, что именно теряется.
62
+ - **Не маппится вовсе** (нет такого типа узла/действия в Retensy — см. schema.md) — **не выдумывай
63
+ обходной путь молча**. Перечисли пользователю такие блоки списком («в источнике был блок X — в
64
+ Retensy аналога нет; варианты: пропустить / заменить на Y / реализовать иначе») и жди решения,
65
+ прежде чем собирать граф целиком. Это тот же принцип, что в `build-bot-funnel`: отказ инструмента
66
+ на несуществующую возможность уже регистрируется автоматически на сервере — тебе отдельно
67
+ дублировать это не нужно, просто не подменяй отсутствующее самоделкой.
68
+
69
+ Не выдумывай маркетинговые тексты за пользователя — если текст сообщения не виден в источнике
70
+ (обрезан на скриншоте, не указан в экспорте), спроси его или пометь `[ВСТАВЬ ТЕКСТ]` только если
71
+ пользователь сам разрешил так делать.
72
+
73
+ ### 4. Построй граф
74
+
75
+ Дальше — как в `build-bot-funnel`: дерево узлов и рёбер, у каждой кнопки-выбора свой `btn_N`,
76
+ валидные UUID, хотя бы один корневой `TRIGGER_*`, без циклов вне `ASK_QUESTION`/`DELAY`/`SCHEDULE`,
77
+ без узлов `END`. Все детали типов/конфигов — в schema.md, не здесь (чтобы не держать два
78
+ расходящихся описания одного формата).
79
+
80
+ Особенность переноса: исходная воронка почти всегда имела **одну** точку входа (команда/кнопка
81
+ запуска у источника). Замени её на подходящий Retensy-триггер по платформе назначения
82
+ (`TRIGGER_COMMAND` для Telegram/MAX, `TRIGGER_IG_DM`/`TRIGGER_IG_COMMENT`/... для Instagram) — см.
83
+ раздел триггеров в schema.md и платформенные отличия в самом начале `build-bot-funnel/SKILL.md`.
84
+
85
+ ### 5. Проверь локально
86
+
87
+ ```bash
88
+ node "<путь к build-bot-funnel>/validate.mjs" <путь к import.json>
89
+ # Для IG-бота:
90
+ node "<путь к build-bot-funnel>/validate.mjs" <путь к import.json> --platform=INSTAGRAM
91
+ # Для MAX-бота:
92
+ node "<путь к build-bot-funnel>/validate.mjs" <путь к import.json> --platform=MAX
93
+ ```
94
+
95
+ Исправь всё, что покажет валидатор, прежде чем предлагать заливку.
96
+
97
+ ### 6. Залей как НОВЫЙ черновик — никогда не перезаписывай существующее
98
+
99
+ Это главное отличие переноса из другого конструктора от обычной правки:
100
+ - **Всегда `create_graph` (новый `graphId`) или `create_graph_from_template`**, затем `update_graph`.
101
+ Либо `import_funnel(botId, name, graph)` — он сам делает create → update → dry-run → publish, но
102
+ publish из него всё равно требует подтверждения (см. п.7) — не вызывай `import_funnel` раньше, чем
103
+ пользователь согласился на публикацию, либо вызови и объясни, что опубликованный результат — это
104
+ черновик, который он должен подтвердить, ещё раз, прежде чем переключать его в активный.
105
+ - **Никогда не используй `edit_graph_live`, `patch_graph` или PUT в существующий `graphId`** для
106
+ результата переноса — это чужая, параллельно создаваемая воронка, а не правка уже работающего
107
+ сценария. Если у бота есть похожий/одноимённый старый граф — не трогай его.
108
+ - Назови новый граф так, чтобы было видно происхождение, например `"Перенос из BotHelp — <название>"`.
109
+ - Прогони `dry_run(graphId, kind:"command", value:"start")` (или подходящий триггер для IG) и
110
+ разбери `runStatus`.
111
+
112
+ ### 7. Публикуй только после подтверждения пользователя
113
+
114
+ Собранный и провалидированный черновик — не значит «готово к продакшену». Покажи пользователю: сколько
115
+ узлов/веток получилось, что промаплено с потерей (из п.3), что попало в `[ВСТАВЬ ТЕКСТ]`, ссылку/id
116
+ графа. **Публикуй (`publish_graph`) и переключай на активный (`set_active_graph`) только по явному
117
+ «ОК, публикуй» пользователя** — перенос почти всегда замещает текущую рабочую воронку бота, ошибка
118
+ здесь стоит дороже, чем при сборке воронки с нуля.
119
+
120
+ ### 8. (Опционально) перенос подписчиков
121
+
122
+ Перенос графа не переносит людей — это отдельный процесс через `migration_*` инструменты (CSV):
123
+ - `migration_presets()` — какие источники/пресеты колонок уже знает сервис (BotHelp, SaleBot, Senler,
124
+ Leadteh, Smartsender, ManyChat, универсальный CSV).
125
+ - `migration_preview(botId, path, platform, mapping?)` — без `mapping` вернёт подсказку сопоставления
126
+ колонок; с `mapping` — полный dry-run (ничего не пишет): сколько новых, сколько уже есть, нет ли
127
+ превышения лимита подписчиков тарифа.
128
+ - `migration_commit(botId, path, platform, mapping)` — реальный импорт тем же `mapping`, что
129
+ подтвердил `migration_preview`. Делай только после явного согласия пользователя, как и публикацию
130
+ графа.
131
+
132
+ Экспорт воронки (структуры сценария) ни один из этих сервисов документированно не отдаёт — поэтому
133
+ граф всегда собирается по п.1–7, а `migration_*` покрывает только список подписчиков (CSV).
134
+
135
+ ## Важные ограничения
136
+
137
+ - Если источник прислал JSON с кастомными типами блоков, которых нет у нас — **сначала** сверься со
138
+ schema.md, **потом** спрашивай пользователя (п.3). Никогда не придумывай тип узла по аналогии.
139
+ - Как и `build-bot-funnel`, при распознавании с приватной доски (Miro и т.п.) надёжнее всего
140
+ CSV-экспорт из неё самой, а не попытка распознать скриншот холста.
141
+ - Все лимиты и правила валидации графа (размер ≤4 МБ, длины текста, платные узлы/действия, лимиты
142
+ переменных) — те же, что в `build-bot-funnel`; не дублируются здесь.
143
+
144
+ ## Файлы скилла
145
+
146
+ - [reference/mapping.md](reference/mapping.md) — таблица сопоставления понятий (сообщения, кнопки,
147
+ задержки, теги, переменные, условия, оплаты, формы) с реальными узлами Retensy + ловушки по
148
+ конкретным источникам (BotHelp, SaleBot, Senler, Leadteh, Smartsender, ManyChat, PuzzleBot).
149
+ - [`../build-bot-funnel/reference/schema.md`](../build-bot-funnel/reference/schema.md) — формат графа
150
+ и единственный источник правды о типах узлов.
151
+ - [`../build-bot-funnel/reference/validation.md`](../build-bot-funnel/reference/validation.md) —
152
+ правила валидатора и коды ошибок.
153
+ - [`../build-bot-funnel/validate.mjs`](../build-bot-funnel/validate.mjs) — используй его же, отдельного
154
+ валидатора у этого скилла нет.
@@ -0,0 +1,95 @@
1
+ # Таблица сопоставления: другой конструктор → Retensy
2
+
3
+ Общие понятия, которые есть почти в любом bot-builder'е, и к чему они маппятся в `retensy-bot-graph`.
4
+ Полные конфиги и правила валидатора — в [`../../build-bot-funnel/reference/schema.md`](../../build-bot-funnel/reference/schema.md)
5
+ и [`validation.md`](../../build-bot-funnel/reference/validation.md) — здесь только «куда класть», не дублируем поля.
6
+
7
+ ## Таблица по концепциям
8
+
9
+ | Концепция в источнике | Узел/конфиг Retensy | Примечания |
10
+ |---|---|---|
11
+ | Точка входа / команда запуска / приветственное сообщение | `TRIGGER_COMMAND` (`/start` и т.п., Telegram/MAX) | Для Instagram входа нет — `TRIGGER_IG_DM`/`TRIGGER_IG_COMMENT`/`TRIGGER_IG_STORY_REPLY`/`TRIGGER_IG_STORY_MENTION` |
12
+ | Триггер по ключевому слову/тексту | `TRIGGER_TEXT` (`matchMode: ANY/EQUALS/CONTAINS/REGEX`) | |
13
+ | Триггер по нажатию инлайн-кнопки (legacy callback без нашего авто-handle) | `TRIGGER_CALLBACK` | Для НОВЫХ кнопок-выборов отдельный триггер не нужен — используй рёбра `btn_N` от `SEND_MESSAGE` |
14
+ | Сообщение / текстовый блок / карточка | `SEND_MESSAGE` (`config.text` + `cards[0]`) | Оба поля обязательны, см. `SEND_NO_TEXT` в validation.md |
15
+ | Сообщение с фото/видео/документом/голосовым/кружком | `SEND_MESSAGE` с медиа-карточкой (`cards[].type`) или `SEND_PHOTO` | Файл заранее через `upload_file`, в графе хранится только URL |
16
+ | Альбом/галерея фото | `SEND_MESSAGE` карточка `type:"gallery"`, `urls[]` | 2–10 картинок |
17
+ | Инлайн-кнопка с выбором (ведёт к ветке сценария) | Кнопка `kind:"CALLBACK"` на `SEND_MESSAGE`, `value:""`, ребро `btn_N` | Непустой `value` ломает переход — см. `validation.md` |
18
+ | Кнопка-ссылка (переход на внешний сайт) | Кнопка `kind:"URL"`, `value` = ссылка; `track:true` если нужно считать клики | Условие по клику — `CONDITION kind:"LINK_CLICKED"` |
19
+ | Вопрос с ожиданием свободного ответа / сбор данных (имя/телефон/email) | `ASK_QUESTION` или `SEND_MESSAGE` с `awaitReply:true` + `saveTo`/`inputKind`/`validator` | Выходы `valid`/`invalid` |
20
+ | Задержка / «подождать N часов/дней» | `DELAY kind:"FIXED"` (`durationSec`) | |
21
+ | Отправка «завтра в HH:MM» | `DELAY kind:"TOMORROW"` | |
22
+ | Отправка в конкретный момент / «через сутки после записи» | `DELAY kind:"UNTIL"` (`isoTimestamp`, опц. `offset`) | |
23
+ | Запуск по расписанию (cron, ежедневная рассылка сводки и т.п.) | `TRIGGER_SCHEDULE` (`cron`, `timezone`) | Headless — без чата, отправка только через `config.target` |
24
+ | Тег / метка подписчика | `ADD_TAG` / `REMOVE_TAG`, условие `CONDITION kind:"TAG"` | `tag` ∈ `[a-z0-9_-]{1,64}` |
25
+ | Переменная / поле подписчика (custom field) | `SET_VARIABLE`, условие `CONDITION kind:"VARIABLE"` | `key` ∈ `[a-z_][a-z0-9_]{0,63}` |
26
+ | Условие «если/иначе» (бинарное) | `CONDITION` (выходы `yes`/`no`) | `match: ALL/ANY` по списку `conditions[]` |
27
+ | Условие со случайным сплитом (A/B тест) | `BRANCH` (`abTest:true`, выходы `case_<id>`) | |
28
+ | Ветвление по значению переменной/коду ответа (больше двух исходов) | `SWITCH` (`expression`, `cases[].value`, выходы `case_<id>`+`default`) | Детерминированный выбор, не случайный |
29
+ | Условие «подписан ли на канал» | `CONDITION kind:"SUBSCRIBED"` | Только Telegram/MAX |
30
+ | Условие по UTM-метке | `CONDITION kind:"UTM"` | |
31
+ | HTTP-запрос / вебхук наружу | Действие `external_request` в блоке `ACTIONS` | Платное действие; своих выходов нет — ветвиться по коду ответа через `saveStatusTo`+`SWITCH` |
32
+ | Входящий вебхук от стороннего сервиса | `TRIGGER_WEBHOOK` (+ `provider`/`connectionId`/`event` для готовых интеграций) | |
33
+ | Оплата / приём платежа | Действие `issue_invoice` в `ACTIONS` (НЕ отдельный узел) | `connectionId` подключения `provider=YOOKASSA`; ветка `paid` — если нужно ждать оплату в диалоге |
34
+ | Триггер «оплата прошла» | `TRIGGER_PAYMENT` | Фильтр `minAmount`/`descriptionContains` обязателен, иначе срабатывает на любую оплату |
35
+ | Форма на сайте / лендинге | `TRIGGER_SITE_FORM` (`siteId`, `formId`) | Только для своего опубликованного сайта в Retensy |
36
+ | Интеграция с CRM (amoCRM, Битрикс24, GetCourse и т.п.) | Действия `amocrm_send`/`bitrix24_call`/`getcourse_send`/... в `ACTIONS` | Нужен `connectionId` из `list_integrations` |
37
+ | Передача диалога оператору / «позвать менеджера» | Действие `agent_chat` в `ACTIONS` | Возврат — `dialog_handoff {active:false}` |
38
+ | AI-ответчик / встроенный GPT-блок | `AI_REPLY` (`mode: "simple"` или `"agent"`) | Платный узел на любом тарифе; у режима `agent` нет выхода `next`, только именованные ветки |
39
+ | Переход в другой сценарий / подворонку | Действие `call_scenario` в `ACTIONS` (`mode: "goto"`/`"call"`) | Ставится последним действием блока |
40
+ | Рассылка по сегменту/тегу | Отдельный инструмент `broadcast_send`, НЕ узел графа | Описание — скилл `send-broadcast` |
41
+ | Конец ветки / «стоп» | Ничего — ветка просто не имеет исходящих рёбер | **Не добавляй** узел `END`, его больше нет в палитре |
42
+
43
+ ## Особенности конкретных источников
44
+
45
+ Эти сервисы не публикуют документированный формат экспорта воронки/сценария (только список
46
+ подписчиков CSV-экспортом) — поэтому граф почти всегда переносится по скриншотам/пересказу, а не
47
+ по машинному JSON. Ниже — на что обращать внимание при разборе каждого.
48
+
49
+ - **BotHelp** — сценарии строятся из «блоков» с условными переходами по тегам и триггерам ключевых
50
+ слов; у BotHelp есть «цепочки» (broadcast-подобные серии сообщений с задержками) — переносятся как
51
+ последовательность `SEND_MESSAGE` + `DELAY`, а не как единая рассылка. Переменные BotHelp
52
+ (`{{переменная}}`) — проверь синтаксис конкретных полей (имя/телефон/UTM) и сопоставь с нашими
53
+ `{{var.*}}`/`{{from.*}}`.
54
+ - **SaleBot** — активно использует HTTP-блоки («Внешний запрос») и JS-код внутри сценария. Код
55
+ (кастомная логика на JS) не маппится напрямую — спроси пользователя, что конкретно он делает, и
56
+ реализуй через `external_request`/`FORMULA`/`CONDITION`, либо зафиксируй как немаппируемое.
57
+ - **Senler** — завязан на ВКонтакте (группы, UTM ВК). У Retensy VK — это публикации на стену
58
+ (`vk_post`), а не полноценный бот-конструктор ВК: диалоговые сценарии Senler ВК переносятся в
59
+ Telegram/MAX/Instagram-бота Retensy, это не 1:1 перенос платформы, предупреди пользователя.
60
+ - **Leadteh** — похож на BotHelp по модели «блоки + условия по тегам»; отдельно стоит «воронка
61
+ продаж» (канбан-доска сделок) — это НЕ часть графа сценария, в Retensy ближайший аналог —
62
+ интеграция с внешней CRM (amoCRM/Битрикс24) через `ACTIONS`, не переноси как узлы графа.
63
+ - **Smartsender** — мультиканальный (email + мессенджеры + SMS); переноси только мессенджер-ветку в
64
+ граф, email/SMS-рассылки за пределами диалога — отдельный процесс (не маппятся в узлы графа).
65
+ - **ManyChat** — активно использует «Growth Tools» (виджеты входа с разных площадок — Ref URL, Click
66
+ Widget, Facebook Comments) и условные Smart Delay. Смарт-триггеры входа превращаются в один
67
+ подходящий `TRIGGER_*` Retensy (чаще `TRIGGER_COMMAND`/`TRIGGER_TEXT`); «Customer Chat» элементы
68
+ (facebook-специфика) обычно немаппируемы — фиксируй в корзине «не маппится».
69
+ - **PuzzleBot** — конструктор на основе «кубиков» с явными condition-блоками по переменным/рефералам;
70
+ реферальная система (учёт кто кого привёл) не имеет прямого аналога в Retensy — переносится как
71
+ `SET_VARIABLE`/`CONDITION` вручную по переменной, зафиксируй ограничение пользователю.
72
+ - **Любой другой источник, не названный выше** — та же методика: инвентарь → сопоставление по
73
+ смыслу шага (не по названию блока) → немаппируемое в отдельный список на подтверждение.
74
+
75
+ ## Частые ловушки при переносе (из любого источника)
76
+
77
+ - **Кнопка-выбор с непустым `value`.** В исходнике кнопка часто хранит «значение» (тег/id ветки) —
78
+ при переносе в CALLBACK-кнопку Retensy это значение **не** переносится в поле `value` (оно должно
79
+ остаться `""`), переход задаётся только ребром `btn_N`. Если очень нужно явное значение — это
80
+ отдельный узел `TRIGGER_CALLBACK`, а не кнопка-выбор.
81
+ - **Задержки в других единицах.** Источники часто пишут задержку как «1440 минут» или «1 день» —
82
+ переведи в секунды (`durationSec`) аккуратно, не копируй число как есть в другое поле.
83
+ - **HTML/markdown разметка текста.** Форматирование источника (жирный/курсив/ссылки) почти никогда
84
+ не совпадает байт-в-байт с безопасным Telegram-HTML Retensy (`b,strong,i,em,u,ins,s,strike,del,code,pre,a[href],tg-spoiler,br`).
85
+ Если не уверен, что разметка пройдёт — ставь `parseMode:"PLAIN"` и убери теги.
86
+ Пустой экран над таблицей выбора кнопок — тоже источник незаметной потери контента, см. п.1 скилла
87
+ (сначала инвентарь, потом граф).
88
+ - **«Условие» в источнике — не всегда `CONDITION`.** Если исходный блок на самом деле случайно
89
+ делит аудиторию (A/B) — это `BRANCH`, а не `CONDITION`; если выбирает по значению из списка
90
+ вариантов — это `SWITCH`. Смотри поведение, не название.
91
+ - **Платные блоки источника vs платные узлы Retensy не совпадают 1:1.** AI-блок / внешний запрос /
92
+ оплата — в Retensy тоже платные (`PREMIUM_NODE_FORBIDDEN`/`PREMIUM_NODES`) независимо от того,
93
+ был ли эквивалент бесплатным в источнике — предупреди пользователя при переносе таких блоков.
94
+ - **Не копируй ID узлов источника как ID узлов Retensy.** Нужны валидные UUID (п.4 skill) —
95
+ генерируй новые, при необходимости сохраняя исходные имена/заголовки в `_title` для трассируемости.
package/src/index.mjs CHANGED
@@ -27,7 +27,7 @@ import os from "node:os";
27
27
  import fs from "node:fs";
28
28
  import path from "node:path";
29
29
 
30
- const VERSION = "0.23.0";
30
+ const VERSION = "0.24.0";
31
31
  /** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
32
32
  const SITE_INITS = ["starter", "blank", "mini-landing"];
33
33
  /** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
@@ -889,7 +889,7 @@ const TOOLS = [
889
889
  { name: "rename_graph", description: "Переименовать сценарий (работает и для опубликованных — имя не влияет на исполнение).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, name: { type: "string" } }, required: ["graphId", "name"] } },
890
890
  { name: "clone_graph", description: "Склонировать граф в новый DRAFT «… (copy)» — безопасно итерировать поверх опубликованного. Клон сценария-вебхука получает собственный путь и секрет вебхука.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
891
891
  { name: "copy_graph", description: "Скопировать граф в ДРУГОГО бота (в т.ч. на другую платформу). Возвращает {graphId, sourcePlatform, targetPlatform, notes[]}. notes[] помечают, что адаптировано (severity=TRANSFORM, напр. вопрос-контакт → ввод телефона текстом), что требует ручной правки (MANUAL, напр. условие SUBSCRIBED в MAX) и особенности платформы (INFO). Авто-адаптация узлов реализована для Telegram⇄MAX; при копировании в/из Instagram-бота граф копируется без трансформаций — несовместимые узлы будут отмечены при публикации (IG-allowlist). preview=true — только проверка совместимости, без копирования. Тот же бот запрещён (для дублирования есть clone_graph). Копия сценария-вебхука становится обычным сценарием бота-получателя (вход по вебхуку не переносится).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, targetBotId: { type: "string", description: "id бота-получателя (см. list_bots)" }, preview: { type: "boolean", description: "true = только отчёт о совместимости, ничего не сохраняется" } }, required: ["graphId", "targetBotId"] } },
892
- { name: "delete_graph", description: "Удалить граф. Активный (опубликованный и назначенный боту) удалить нельзя — будет 409; сначала переключи активный через set_active_graph.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
892
+ { name: "delete_graph", description: "Удалить граф (DELETE /api/bots/graphs/{id}) — тот же эндпоинт удаляет и вебхук-сценарий (scenarioId из scenario_list/scenario_create — это id того же графа). Активный (опубликованный и назначенный боту) удалить нельзя — будет 409; сначала переключи активный через set_active_graph. Необратимо.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
893
893
  { name: "set_active_graph", description: "Назначить, какой опубликованный граф активен у бота (переключение живого сценария без перепубликации). HTTP 409 — граф не опубликован или это сценарий-вебхук (источник WEBHOOK — такой включается своей публикацией в вебе).", inputSchema: { type: "object", properties: { botId: { type: "string" }, graphId: { type: "string" } }, required: ["botId", "graphId"] } },
894
894
  { name: "upload_file", description: "Загрузить файл в библиотеку /bots/files (POST /api/bots/media) и получить публичный URL для вставки в сценарий. Передай path (локальный файл) ИЛИ url (перезалить файл по ссылке в своё хранилище). Возвращает {id, url, mediaType, sizeBytes, originalName}. Полученный url ставь в медиа-карточку SEND_MESSAGE (image/video/audio/file/voice/videonote → поле url; gallery → urls[]) или в SEND_PHOTO.photoUrl. Лимит 50 МБ; типы: image/video/audio/pdf/zip/doc(x)/xlsx/pptx/txt (SVG запрещён); при нехватке места — HTTP 402.", inputSchema: { type: "object", properties: { path: { type: "string", description: "Путь к локальному файлу (поддерживается ~)" }, url: { type: "string", description: "Ссылка на файл — будет скачан и перезалит в /bots/files" }, filename: { type: "string", description: "Переопределить имя файла (необязательно)" } } } },
895
895
  { name: "list_files", description: "Список файлов в библиотеке /bots/files (GET /api/bots/media) + использовано/лимит байт. Бери готовые url отсюда, чтобы не загружать одно и то же повторно.", inputSchema: { type: "object", properties: {} } },
@@ -1052,6 +1052,20 @@ const TOOLS = [
1052
1052
  { name: "agent_tool_test", description: "Проверить инструмент агента напрямую, БЕЗ модели (POST /api/bots/agents/{agentId}/tools/{toolId}/test): params — аргументы вызова (AMO_LOOKUP: query ИЛИ leadId; SHEETS_LOOKUP: value; HTTP_API: имена из httpParams этого инструмента). Проверяет только конфиг инструмента (подключение amoCRM/таблицу Sheets/URL) — модель не вызывается, бюджет ИИ не тратится. {ok:true, result} при успехе; ошибка конфига/внешнего сервиса — HTTP 400 {ok:false, error} с причиной на русском (никогда не 500). toolId — id инструмента из agent_get → tools[].id (настраивается через agent_update {patch:{tools:[...]}})." , inputSchema: { type: "object", properties: { agentId: { type: "string" }, toolId: { type: "string" }, params: { type: "object", description: "Аргументы вызова инструмента (см. описание)" } }, required: ["agentId", "toolId"] } },
1053
1053
  // ---- Заявки сайта: файлы вложений ----
1054
1054
  { name: "site_lead_file_download", description: "Метаданные и ссылка на скачивание файла из заявки сайта (GET /api/bots/pages/{siteId}/leads/{leadId}/files/{fileId}, владелец/ADMIN сайта). Байты файла НЕ передаются через MCP (может быть большим/бинарным) — инструмент делает HEAD-запрос, чтобы подтвердить, что файл принадлежит этой заявке (404 иначе), и вернуть {fileId, fileName, contentType, sizeBytes, downloadUrl}. downloadUrl отдаёт файл только с тем же токеном, что у этого MCP (заголовок Authorization: Bearer zmcp_...), — скачай curl'ом с этим заголовком или открой заявку в кабинете (сайт → «Заявки»). Список файлов заявки с теми же полями уже есть в site_leads (поле files[] каждой заявки) — этот инструмент нужен, только когда нужна именно прямая ссылка на скачивание одного файла.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, fileId: { type: "string" } }, required: ["siteId", "leadId", "fileId"] } },
1055
+ // ---- Вебхук-сценарии (/api/scenarios, PAT-доступ открыт решением владельца 14.10) ----
1056
+ { name: "scenario_list", description: "Сценарии автора (GET /api/scenarios): [{id, name, status, active, sourceType: BOT|WEBHOOK, botId}] — включая вебхук-сценарии (в list_graphs их нет, см. его описание). sourceType WEBHOOK — свой вход по адресу (path+secret в scenario_get), выход — произвольный бот через scenario_update_source; BOT — живёт в графах привязанного бота (тот же объект, что и в list_graphs/get_graph этого бота). Read-only.", inputSchema: { type: "object", properties: {} } },
1057
+ { name: "scenario_create", description: "Создать сценарий (POST /api/scenarios), пустым — узлы/рёбра потом через update_graph/edit_graph_live по id из ответа. botId не передан → вебхук-сценарий (source.type WEBHOOK): сервер сам выпускает путь входа и секрет (видны в scenario_get, ротируются scenario_rotate_secret), выходной бот задаётся отдельно через scenario_update_source. botId передан → бот-сценарий (source.type BOT), сразу привязан к этому боту — нужен доступ на редактирование его графов (owner/EDITOR/ADMIN), иначе 403/404; публикуется он уже через publish_graph, не через scenario_publish. Лимит сценариев тарифа — HTTP 402.", inputSchema: { type: "object", properties: { name: { type: "string" }, botId: { type: "string", description: "UUID бота — делает сценарий бот-сценарием вместо вебхук-сценария" } }, required: ["name"] } },
1058
+ { name: "scenario_get", description: "Сценарий по id (GET /api/scenarios/{id}) — тот же граф, что get_graph, плюс source: {type: BOT|WEBHOOK, config: у WEBHOOK path/secret входа и (если задан) botId выхода, у BOT — только botId} и outputBinding {channel, recipientFrom}. Свой сценарий — всегда; чужой бот-сценарий — если есть доступ к боту (как GET .../graphs/{id}); чужой вебхук-сценарий — 404 (в нём секрет входа, виден только автору).", inputSchema: { type: "object", properties: { scenarioId: { type: "string" } }, required: ["scenarioId"] } },
1059
+ { name: "scenario_update_source", description: "Привязка выхода вебхук-сценария, ЦЕЛИКОМ заменяет текущую (PUT /api/scenarios/{id}/source) — только у source.type WEBHOOK, у BOT-сценария 409. botId — бот-выход (нужен доступ на редактирование его графов — owner/EDITOR/ADMIN, иначе 403/404); НЕ передан → привязка снимается, сценарий остаётся headless без бота. channel — OutputChannel: TG_BOT_API (обычный Bot API Telegram/MAX) | TG_USERBOT | IG | MAX; не передан — прежнее значение сохраняется. recipientFrom — откуда брать chatId получателя (например \"CHAT\" — чат-инициатор, или имя переменной сценария); НЕ передан → очищается в пустую строку (поле не частичное — шли его всегда, если хочешь сохранить текущее значение, сначала прочитай scenario_get). У опубликованного сценария новая привязка применяется сразу; если она добавляет НОВЫЕ ошибки проверки (платформа/лимит), которых не было при старой, — HTTP 422 со всеми code@nodeId, ничего не сохранено (уже имевшиеся ошибки не блокируют).", inputSchema: { type: "object", properties: { scenarioId: { type: "string" }, botId: { type: "string", description: "UUID бота-выхода; опусти, чтобы снять привязку" }, channel: { type: "string", enum: ["TG_BOT_API", "TG_USERBOT", "IG", "MAX"] }, recipientFrom: { type: "string", description: "Не передашь — очистится в пустую строку" } }, required: ["scenarioId"] } },
1060
+ { name: "scenario_publish", description: "Опубликовать вебхук-сценарий (POST /api/scenarios/{id}/publish) — только source.type WEBHOOK (у BOT-сценария 409, публикуй через publish_graph). Те же проверки, что у publish_graph (узлы/лимиты/платформа выходного бота, если он задан): отказ → HTTP 422 со всеми причинами построчно (code@nodeId: message), ничего не опубликовано.", inputSchema: { type: "object", properties: { scenarioId: { type: "string" } }, required: ["scenarioId"] } },
1061
+ { name: "scenario_rotate_secret", description: "Выпустить новый секрет входа вебхук-сценария (POST /api/scenarios/{id}/rotate-secret) — старый сразу перестаёт приниматься. Секрет виден в ответе этого вызова и в scenario_get (source.config.secret) — это собственный секрет сценария для настройки его входящего вебхука, не чужой секрет интеграции.", inputSchema: { type: "object", properties: { scenarioId: { type: "string" } }, required: ["scenarioId"] } },
1062
+ // ---- Публичный API ИИ-агента: ключи (решение владельца 14.10) ----
1063
+ { name: "agent_api_key_create", description: "Выпустить API-ключ публичного чата агента (POST /api/bots/agents/{agentId}/api-keys): один ключ работает только с ЭТИМ агентом (не аккаунтом целиком, в отличие от zmcp-токена). Секрет (вид zagt_...) виден ТОЛЬКО в этом ответе — дальше хранится лишь хеш; сохрани/покажи пользователю сразу, повторно его не получить. До 20 активных ключей на агента — сверх лимита HTTP 409 TOO_MANY_KEYS, сначала отзови лишний (agent_api_key_revoke). Ключ шлют заголовком Authorization: Bearer <ключ> на POST /api/public/agents/{agentId}/chat (публичный чат агента, без zmcp-токена/сессии) — {conversationId?, message (1–1000 симв.), metadata?} → {reply, conversationId, sources?}; 30 запросов/мин на ключ (429 иначе), агент должен быть опубликован (agent_publish).", inputSchema: { type: "object", properties: { agentId: { type: "string" }, name: { type: "string", description: "Название ключа для списка, например «Виджет на сайте»" } }, required: ["agentId"] } },
1064
+ { name: "agent_api_key_list", description: "Список API-ключей агента (GET /api/bots/agents/{agentId}/api-keys): {id, name, keyPrefix, createdAt, lastUsedAt} — без секрета (key:null), секрет виден только один раз при agent_api_key_create. Read-only.", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
1065
+ { name: "agent_api_key_revoke", description: "Отозвать API-ключ агента (DELETE /api/bots/agents/{agentId}/api-keys/{keyId}) — публичный чат с этим ключом сразу перестаёт отвечать (401). keyId — из agent_api_key_list. Необратимо: только по явной просьбе пользователя, с confirm:true.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, keyId: { type: "string" }, confirm: { type: "boolean" } }, required: ["agentId", "keyId", "confirm"] } },
1066
+ // ---- Пакеты ответов ИИ сверх тарифа (решение владельца 14.10) ----
1067
+ { name: "ai_packs", description: "Пакеты ответов ИИ сверх месячного бюджета тарифа: каталог (GET /api/bots/ai-packs) — [{sku, answers, priceRub, kopecks}] (ai_pack_1000 — 1000 ответов/490₽, ai_pack_5000 — 5000/1990₽, ai_pack_20000 — 20000/6990₽; kopecks — внутренняя единица бюджета, справочно, не присылай её). Плюс баланс — aiPackAnswersLeft/aiPackTokensLeft из GET /api/bots/subscription: пакет не сгорает без активного тарифа, но списывается только при нём, ПОСЛЕ месячного бюджета тарифа, FIFO по старшинству покупки (сначала самый старый). Read-only; купить — ai_pack_purchase.", inputSchema: { type: "object", properties: {} } },
1068
+ { name: "ai_pack_purchase", description: "ЭТО ПЛАТЁЖ, не покупка напрямую. Купить пакет ответов ИИ (POST /api/bots/ai-packs/purchase {sku, method}): тот же конвейер оплаты (ЮKassa/крипта), что у смены тарифа — ответ НЕ списывает деньги сам, а возвращает invoiceUrl, ссылку на оплату; пакет зачислится на баланс только после подтверждения платежа пользователем в браузере. sku — из ai_packs; method — \"card\" (по умолчанию), как у subscribe. Только по явной просьбе пользователя, с confirm:true.", inputSchema: { type: "object", properties: { sku: { type: "string", description: "sku из ai_packs, например ai_pack_5000" }, method: { type: "string", description: "card (по умолчанию) и т.п., как у subscribe" }, confirm: { type: "boolean" } }, required: ["sku", "confirm"] } },
1055
1069
  ];
1056
1070
 
1057
1071
  async function handleCall(params) {
@@ -2112,6 +2126,77 @@ async function handleCall(params) {
2112
2126
  const meta = await siteLeadFileHead(siteId, leadId, fileId);
2113
2127
  return okResult({ ...meta, authHint: "Скачать: curl -H \"Authorization: Bearer <твой zmcp_-токен>\" \"<downloadUrl>\" -o файл — либо открой заявку в кабинете (сайт → «Заявки»)." });
2114
2128
  }
2129
+ // ---- Вебхук-сценарии ----
2130
+ case "scenario_list": return okResult(await api("/api/scenarios"));
2131
+ case "scenario_create": {
2132
+ const name = String(a.name ?? "").trim();
2133
+ if (!name) throw new Error("Передай name — название сценария.");
2134
+ const source = a.botId != null && String(a.botId).trim() !== ""
2135
+ ? { type: "BOT", config: { botId: uuidArg(a.botId, "botId") } }
2136
+ : { type: "WEBHOOK", config: {} };
2137
+ return okResult(await api("/api/scenarios", { method: "POST", body: { name, source } }));
2138
+ }
2139
+ case "scenario_get": return okResult(await api(`/api/scenarios/${uuidArg(a.scenarioId, "scenarioId")}`));
2140
+ case "scenario_update_source": {
2141
+ const scenarioId = uuidArg(a.scenarioId, "scenarioId");
2142
+ const body = {
2143
+ botId: a.botId != null && String(a.botId).trim() !== "" ? uuidArg(a.botId, "botId") : null,
2144
+ channel: a.channel != null ? enumArg(a.channel, "channel", ["TG_BOT_API", "TG_USERBOT", "IG", "MAX"]) : null,
2145
+ recipientFrom: a.recipientFrom != null ? String(a.recipientFrom) : null,
2146
+ };
2147
+ try {
2148
+ return okResult(await api(`/api/scenarios/${scenarioId}/source`, { method: "PUT", body }));
2149
+ } catch (e) {
2150
+ if (e.status === 409) throw new Error("Это не вебхук-сценарий (source.type BOT) — у бот-сценария выход не перепривязывается, это сам бот графа.");
2151
+ throw e;
2152
+ }
2153
+ }
2154
+ case "scenario_publish": {
2155
+ try {
2156
+ return okResult(await api(`/api/scenarios/${uuidArg(a.scenarioId, "scenarioId")}/publish`, { method: "POST" }));
2157
+ } catch (e) {
2158
+ if (e.status === 409) throw new Error("Это не вебхук-сценарий (source.type BOT) — публикуй через publish_graph.");
2159
+ throw e;
2160
+ }
2161
+ }
2162
+ case "scenario_rotate_secret": return okResult(await api(`/api/scenarios/${uuidArg(a.scenarioId, "scenarioId")}/rotate-secret`, { method: "POST" }));
2163
+ // ---- Публичный API ИИ-агента: ключи ----
2164
+ case "agent_api_key_list": return okResult(await api(`/api/bots/agents/${idArg(a.agentId, "agentId")}/api-keys`));
2165
+ case "agent_api_key_create": {
2166
+ const body = {};
2167
+ if (a.name != null && String(a.name).trim() !== "") body.name = String(a.name).trim();
2168
+ try {
2169
+ return okResult(await api(`/api/bots/agents/${idArg(a.agentId, "agentId")}/api-keys`, { method: "POST", body }));
2170
+ } catch (e) {
2171
+ if (e.status === 409) throw new Error("До 20 активных ключей на агента — сначала отзови лишний (agent_api_key_revoke).");
2172
+ throw e;
2173
+ }
2174
+ }
2175
+ case "agent_api_key_revoke": {
2176
+ const agentId = idArg(a.agentId, "agentId");
2177
+ const keyId = idArg(a.keyId, "keyId");
2178
+ if (a.confirm !== true) throw new Error("Отзыв ключа необратим: публичный чат с ним сразу перестанет отвечать. Спроси пользователя и повтори с confirm:true.");
2179
+ await api(`/api/bots/agents/${agentId}/api-keys/${keyId}`, { method: "DELETE" });
2180
+ return okResult(`🗑️ Ключ ${keyId} отозван.`);
2181
+ }
2182
+ // ---- Пакеты ответов ИИ ----
2183
+ case "ai_packs": {
2184
+ const [skus, sub] = await Promise.all([
2185
+ api("/api/bots/ai-packs"),
2186
+ api("/api/bots/subscription").catch(() => null),
2187
+ ]);
2188
+ return okResult({
2189
+ skus,
2190
+ balance: sub ? { aiPackAnswersLeft: sub.aiPackAnswersLeft, aiPackTokensLeft: sub.aiPackTokensLeft } : null,
2191
+ });
2192
+ }
2193
+ case "ai_pack_purchase": {
2194
+ const sku = String(a.sku ?? "").trim();
2195
+ if (!sku) throw new Error("Передай sku — из ai_packs.");
2196
+ if (a.confirm !== true) throw new Error("Это платёж. Спроси пользователя и повтори с confirm:true.");
2197
+ const r = await api("/api/bots/ai-packs/purchase", { method: "POST", body: { sku, method: a.method || "card" } });
2198
+ return okResult({ ...r, hint: "Пакет зачислится после подтверждения оплаты по invoiceUrl — ничего не списано автоматически." });
2199
+ }
2115
2200
  default:
2116
2201
  throw new Error(`Неизвестный инструмент: ${params && params.name}`);
2117
2202
  }