@retensy/mcp 0.14.0 → 0.15.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.
@@ -6,6 +6,7 @@
6
6
  {
7
7
  "name": "retensy-mcp",
8
8
  "source": "./",
9
+ "version": "0.15.1",
9
10
  "description": "Сборка и публикация воронок ботов (Telegram/MAX, Retensy Bots) из описания, рассылки, подключение ботов и сервисов, сайтов из блоков + публикация статей блога в Markdown — через MCP + скилл."
10
11
  }
11
12
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "retensy-mcp",
3
3
  "displayName": "Retensy MCP",
4
- "version": "0.14.0",
4
+ "version": "0.15.1",
5
5
  "description": "MCP-сервер + скилл для Retensy Bots: подключение ботов и сервисов, сборка и публикация воронок ботов (Telegram/MAX), рассылки, сайты из блоков (Zero-блок, дизайны, шаблоны, свои домены, заявки) и статьи блога в Markdown — всё одним токеном zmcp_.",
6
6
  "author": { "name": "retensy", "url": "https://bots.retensy.com" },
7
7
  "homepage": "https://bots.retensy.com/bots",
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  MCP-сервер (+ скилл для Claude Code) для **сборки и публикации воронок/автоматизаций ботов (Telegram, MAX и Instagram)** в сервисе [retensy `/bots`](https://bots.retensy.com/bots): из текстового описания → валидный граф сценария → заливка и публикация через API.
9
9
 
10
10
  - 🤖 **Сценарии ботов**: `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
- - 🔌 **Боты и сервисы**: `create_bot` (Telegram/MAX по токену), `bot_stop`/`bot_resume`, `connect_integration`/`disconnect_integration` (amoCRM, Битрикс24, GetCourse, Я.Метрика, ЮKassa; Google Таблицы — ссылкой на вход Google).
11
+ - 🔌 **Боты и сервисы**: `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 и живая проверка подключения), `channel_post` (пост в канал Telegram/MAX).
12
12
  - 📣 **Рассылки**: `broadcast_preview` (размер аудитории), `broadcast_send` (сейчас или по расписанию, сразу по нескольким ботам; сообщения или запуск сценария), `broadcast_list`/`broadcast_get`/`broadcast_cancel`, повторяющиеся (`broadcast_recurring`), черновики (`broadcast_drafts`, `broadcast_duplicate`) + скилл `send-broadcast`.
13
13
  - 🔗 **Ссылка вместо отказа**: что нельзя сделать через API (вход через Google/Facebook, оплата тарифа, вход в аккаунт) — инструмент возвращает прямую ссылку и одну строку, что сделать.
14
14
  - 📝 **Статьи блога** (тот же токен `zmcp_…`): `article_publish`, `article_update`, `article_list`, `article_get` — публикация статей в Markdown (как README на GitHub) в раздел **/articles**.
@@ -103,6 +103,10 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
103
103
  | `list_integrations()` | подключённые сервисы (amoCRM, Битрикс24, GetCourse, Я.Метрика, ЮKassa): `id` = `connectionId` для действий сценария |
104
104
  | `connect_integration(provider, creds?, title?, connectionId?)` | подключить/обновить сервис; без кредов — какие поля нужны; Google Таблицы → ссылка входа Google, Instagram → ссылка на кабинет |
105
105
  | `disconnect_integration(connectionId)` | удалить подключение |
106
+ | `integration_catalog()` | каталог Integration Core: поля подключения (`configSchema`) и действия (`actions[].kind`) сервисов |
107
+ | `integration_status(connectionId)` | статус подключения: `OK` / `NEEDS_REAUTH` / `ERROR` / `UNKNOWN`, последняя проверка и ошибка |
108
+ | `integration_test(connectionId)` | живая проверка ключа во внешнем сервисе от имени владельца (без побочных эффектов) |
109
+ | `channel_post(botId, chatId, text?, mediaUrl?)` | разовый пост в канал/группу Telegram или MAX (раздел «Публикации»); файл — из `upload_file` |
106
110
  | `get_graph(graphId, [summary], [saveToFile])` | получить граф; `summary:true` — компактная сводка (id/type/title + рёбра), `saveToFile` — записать полный JSON на диск (для больших графов, чтобы не упереться в лимит токенов) |
107
111
  | `create_graph(botId, name)` | создать пустой граф (DRAFT) |
108
112
  | `update_graph(graphId, graphFile\|graph\|nodes,edges)` | залить узлы/рёбра (PUT); `graphFile` — путь к локальному JSON, граф не нужно слать инлайном |
@@ -124,17 +128,17 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
124
128
  | `list_bot_users(botId)` | подписчики/лиды бота (постранично, поиск `query`) |
125
129
  | `list_links(botId)` | стартовые трекинговые ссылки бота с UTM |
126
130
  | `site_list()` | сайты пользователя (id, mode, url, publishedRevision) |
127
- | `site_create(title, slug?)` | новый сайт из блоков → `id` |
131
+ | `site_create(title, slug?, template?)` | новый сайт из блоков → `id`; `template` (`starter`/`blank`/`mini-landing`) — сразу черновик из шаблона |
128
132
  | `site_get(siteId, saveToFile?)` | модель сайта (`revision`, `draft`, `versions[]` публикаций) |
129
133
  | `site_schema()` | JSON Schema модели и операций — читать перед правкой |
130
- | `site_edit(siteId, ops[], revision?, init?)` | правка операциями, всё или ничего: страницы и папки, блоки, Zero-элементы, код блока (`get/set/add_block_code`), дизайны и их кадры, шаблоны (`add_template`), тема, попапы |
134
+ | `site_edit(siteId, ops[]?, revision?, init?)` | правка операциями (`init`: `starter`/`blank`/`mini-landing`; только `init` — черновик из шаблона), всё или ничего: страницы и папки, блоки, Zero-элементы, код блока (`get/set/add_block_code`), дизайны и их кадры, шаблоны (`add_template`), тема, попапы |
131
135
  | `site_templates(category?, full?)` | библиотека шаблонов блоков для `add_template` |
132
136
  | `site_publish(siteId)` | опубликовать черновик → `url` |
133
137
  | `site_rollback(siteId, revision)` | вернуть прошлую публикацию |
134
138
  | `site_upload_asset(siteId, path\|url)` | картинка/видео в сайт → `assets/…` |
135
139
  | `site_domains(siteId, action, host?, withWww?, domainId?)` | свои домены: list / add / check / remove (число — по тарифу) |
136
140
  | `site_leads(siteId, page?, size?)` | заявки из форм (поля, UTM, статус доставки) |
137
- | `site_lead_settings(siteId, settings?)` | куда доставлять заявки: бот уведомлений, почта, вебхук, вебхук-сценарий, amoCRM |
141
+ | `site_lead_settings(siteId, settings?)` | куда доставлять заявки: бот уведомлений, почта, вебхук, вебхук-сценарий, amoCRM, «Интеграция» (`coreDelivery {connectionId, kind, params}` из `coreConnections`) |
138
142
  | `broadcast_list(botId?, group?, page?, size?)` | рассылки + счётчики разделов (черновики/запланированные/отправленные/повторы) |
139
143
  | `broadcast_get(broadcastId)` | рассылка целиком: статус, счётчики, сообщения |
140
144
  | `broadcast_preview(botIds, tagsAll?, tagsNone?)` | сколько подписчиков получат рассылку |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retensy/mcp",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
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",
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",
25
25
  "validate": "node skills/build-bot-funnel/validate.mjs"
26
26
  },
27
27
  "engines": {
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: build-bot-funnel
3
- description: Собрать воронку (сценарий) бота для сервиса /bots из текстового описания — сгенерировать валидный граф формата retensy-bot-graph, проверить его и (через MCP bot-graph) залить в бота, прогнать dry-run и опубликовать. Поддерживает Telegram, MAX и Instagram. Использовать, когда пользователь описывает воронку/прогрев/сценарий бота словами и просит «собрать», «сделать граф», «залить в бота», «опубликовать сценарий».
3
+ description: Собрать воронку (сценарий) бота для сервиса /bots из текстового описания — сгенерировать валидный граф формата retensy-bot-graph, проверить его и (через MCP bot-graph) залить в бота, прогнать dry-run и опубликовать. Поддерживает Telegram и MAX (Instagram сейчас выключен в сервисе — IG-бота не подключить). Использовать, когда пользователь описывает воронку/прогрев/сценарий бота словами и просит «собрать», «сделать граф», «залить в бота», «опубликовать сценарий».
4
4
  ---
5
5
 
6
6
  # Сборка воронки бота (retensy /bots)
7
7
 
8
- Превращает **текстовое описание воронки** в валидный граф `retensy-bot-graph` и (опционально) публикует его через MCP-сервер `bot-graph`. Поддерживаются платформы **Telegram, MAX и Instagram** — у каждой свои триггеры и ограничения, описанные в schema.md.
8
+ Превращает **текстовое описание воронки** в валидный граф `retensy-bot-graph` и (опционально) публикует его через MCP-сервер `bot-graph`. Поддерживаются платформы **Telegram и MAX** — у каждой свои триггеры и ограничения, описанные в schema.md. **Instagram сейчас выключен в сервисе — IG-бота не подключить**; разделы о нём оставлены на будущее включение.
9
9
 
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 на странице `/bots/instagram` («Подключения» → Instagram), без токена бота.
13
+ - **Instagram** (сейчас выключен в сервисе — IG-бота не подключить) — ограниченный набор узлов; вход только через комментарий/директ/историю (нет `/start`); нет рассылок; DELAY не более 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER/CONTACT (CONTACT = ручной ввод номера, кнопки «Поделиться номером» в IG нет); онбординг через OAuth на странице `/bots/instagram` («Подключения» → Instagram), без токена бота.
14
14
 
15
15
  ## Когда применять
16
16
  Пользователь описывает сценарий бота словами: приветствие → подписка → вопрос с кнопками → ветки → задержки → вебинар и т.п. Либо просит залить/опубликовать готовую воронку.
@@ -39,6 +39,7 @@ description: Собрать воронку (сценарий) бота для с
39
39
  - **не ждать** — ветку `paid` не рисуй, блок просто пришлёт счёт и пойдёт по `next`; выдать доступ потом сможет отдельный сценарий с триггером `TRIGGER_PAYMENT`.
40
40
  Счёт обязан быть **последним** действием в блоке и только одним (`INVOICE_NOT_LAST`, `INVOICE_DUPLICATE`).
41
41
  - **`TRIGGER_PAYMENT` («оплата прошла»)** — если счёт выставлял бот, подписчик определяется САМ (счёт помнит его в metadata платежа): можно сразу слать сообщение и вешать метку, разбирать ответ кассы не нужно. Если оплата пришла мимо бота — прогон headless, получателя задавай через `config.target`. Ставь фильтр `minAmount`/`descriptionContains`, иначе сценарий сработает на любую оплату в кассе.
42
+ - **`TRIGGER_SITE_FORM` («Заявка с сайта»)** — `{siteId, formId}`: посетитель отправил форму на ОПУБЛИКОВАННОМ сайте владельца (`site_list` → id сайта, id формы — из `site_get`). Пустой/отсутствующий `formId` = любая форма сайта. Поля формы — `{{var.<имя поля>}}`; имя, почта, телефон всегда доступны как `{{var.name}}`, `{{var.email}}`, `{{var.phone}}` (если поле названо иначе — находятся по подписи), UTM — `{{var.utm_source}}` и т.п. Чата с посетителем нет: узлы отправки работают только с явным `config.target`, CRM/таблицы/вебхуки — как обычно. Обычно это сценарий без бота (источник WEBHOOK, как вебхук-сценарий) — ему `TRIGGER_WEBHOOK` не нужен; можно и в сценарии бота. Чужой сайт → `SITE_FORM_FOREIGN_SITE`, без сайта → `SITE_FORM_NO_SITE`. Одна заявка — один запуск; `TRIGGER_ANY_UPDATE` на заявки не срабатывает.
42
43
 
43
44
  4. **Проверь локально** перед заливкой:
44
45
  ```bash
@@ -72,6 +72,12 @@
72
72
  если не подошёл ни один конкретный триггер. Удобен как «ничего не понял» / отладка.
73
73
  - `TRIGGER_WEBHOOK` — `{}`, точка входа сценария с источником WEBHOOK (не бот). Такие сценарии
74
74
  создаются в вебе; у графа бота этот триггер не сработает.
75
+ - `TRIGGER_SITE_FORM` — `{ "siteId": "<uuid сайта>", "formId": "<id формы>" }` — заявка с формы
76
+ опубликованного сайта владельца. `formId` пустой/нет = любая форма сайта. `siteId` обязателен
77
+ (`SITE_FORM_NO_SITE`), сайт должен быть свой (`SITE_FORM_FOREIGN_SITE`). Переменные: поля формы
78
+ `{{var.<имя>}}`, всегда `{{var.name}}`/`{{var.email}}`/`{{var.phone}}` (по имени поля, иначе по подписи),
79
+ UTM `{{var.utm_*}}`. Прогон без чата: узлы отправки — только с `config.target`. Работает и в сценарии
80
+ без бота (источник WEBHOOK — тогда `TRIGGER_WEBHOOK` не обязателен), и в сценарии бота; не для Instagram.
75
81
  - ~~`TRIGGER_COMMENT`~~ — **мёртвый тип**: рантайм нигде не выставляет `event="comment"`, сработать
76
82
  он не может. Убран из палитры редактора. Комментарии Instagram — `TRIGGER_IG_COMMENT`.
77
83
 
@@ -361,6 +367,8 @@ IG-боты не поддерживают команды (`/start`). Вход
361
367
 
362
368
  ## Платформа Instagram
363
369
 
370
+ > **Сейчас Instagram выключен в сервисе:** IG-бота не подключить, страница `/bots/instagram` ведёт на `/bots`. Раздел оставлен на будущее включение.
371
+
364
372
  IG-боты подключаются через OAuth на странице **`/bots/instagram`** (раздел «Подключения» → карточка
365
373
  Instagram; прежний раздел «Инструменты роста» / `/growth` расформирован и редиректит) — **без вставки токена вручную**; у IG нет персонального бот-токена. После OAuth бот получает доступ к Messaging API через привязанный Instagram Business/Creator-аккаунт.
366
374
 
@@ -213,6 +213,12 @@ for (const n of nodes) {
213
213
  if (!blank(f.status) && ev !== "chat_member" && ev !== "my_chat_member") warns.push(`${who}: фильтр по статусу работает только у chat_member/my_chat_member.`);
214
214
  break;
215
215
  }
216
+ case "TRIGGER_SITE_FORM":
217
+ // Бэкенд (GraphValidator.validateSiteFormTrigger): siteId обязателен и должен быть UUID своего сайта.
218
+ if (blank(c.siteId)) errors.push(`SITE_FORM_NO_SITE: ${who} — нужен siteId (id сайта из site_list).`);
219
+ else if (!UUID_RE.test(String(c.siteId).trim())) errors.push(`SITE_FORM_NO_SITE: ${who} — siteId «${c.siteId}» не UUID сайта (site_list).`);
220
+ if (c.formId != null && typeof c.formId !== "string") errors.push(`${who}: formId — строка (id формы из site_get) или пусто = любая форма.`);
221
+ break;
216
222
  case "TRIGGER_COMMENT":
217
223
  errors.push(`TRIGGER_COMMENT_DEAD: ${who} — этот тип триггера не сработает никогда (рантайм не выставляет event=comment). Для Instagram используй TRIGGER_IG_COMMENT.`);
218
224
  break;
@@ -14,12 +14,16 @@ description: Собрать сайт или лендинг из блоков в
14
14
  1. `site_schema` — какие блоки и поля бывают (`model`) и какие операции есть (`ops`); `site_templates` — готовые
15
15
  секции из библиотеки.
16
16
  2. `site_create {title}` → `id`. Для правки существующего — `site_list`, `site_get {siteId}`.
17
- 3. Первый `site_edit` с `init`: `starter` — готовый лендинг (шапка, обложка, текст, преимущества, форма, подвал) или
18
- `blank` — пустая главная. Заполни тексты, не выдумывай факты о бизнесе — спрашивай.
17
+ 3. Первый `site_edit` с `init`: `starter` — готовый лендинг (шапка, обложка, текст, преимущества, форма, подвал),
18
+ `blank` — пустая главная или `mini-landing` — мини-лендинг «как Taplink» (см. ниже). Только `init` без `ops` —
19
+ создать черновик из шаблона; то же сразу при создании — `site_create {title, template}`. На сайте с черновиком
20
+ `init` игнорируется. Заполни тексты, не выдумывай факты о бизнесе — спрашивай.
19
21
  4. Картинки — `site_upload_asset {siteId, path|url}` → `assets/…` в поля `image`, `logo`, `icon`, `style.bg.image`.
20
22
  5. `site_get` — проверь модель, `site_publish` — сайт открыт по `url`.
21
23
  6. После публикации: заявки — `site_leads`, куда их слать — `site_lead_settings` (бот уведомлений, почта, вебхук,
22
- вебхук-сценарий бота, amoCRM); свой домен — `site_domains` (`add` → пользователь ставит A-запись на `dnsTarget`
24
+ вебхук-сценарий бота, amoCRM, «Интеграция» — `coreDelivery {connectionId, kind, params?}`: любое подключение из
25
+ `coreConnections`, действие — `kind` из `integration_catalog`; запуск автоматизаций по заявке — сценарий с триггером
26
+ `TRIGGER_SITE_FORM {siteId, formId}`, скилл build-bot-funnel); свой домен — `site_domains` (`add` → пользователь ставит A-запись на `dnsTarget`
23
27
  → `check`); неудачная публикация — `site_rollback {revision}` из `site_get → versions[]`.
24
28
 
25
29
  ## Правила модели
@@ -49,6 +53,21 @@ description: Собрать сайт или лендинг из блоков в
49
53
  поправь и верни `set_block_code {blockId, code}`; новый блок из кода — `add_block_code {container, code}`. У
50
54
  обычных блоков код — JSON `{type, variant, props, style}`. Тот же код пользователь видит во вкладке «Код» редактора.
51
55
 
56
+ ## Мини-лендинг и блок «Кнопки мессенджеров»
57
+
58
+ `init: "mini-landing"` (или `site_create {template: "mini-landing"}`) — одна страница без шапки и подвала, колонка
59
+ 640px, на весь экран блок `type: "messengers"`. Его `props`:
60
+
61
+ - `avatar?` (`assets/…`), `title?`, `text?`;
62
+ - `bots: [{botId, label?, start?}]` — боты владельца по порядку, кнопки собираются при публикации; пусто — все
63
+ активные боты (Telegram, MAX, Instagram). `start` — метка старта (латиница, цифры, `_`, `-`);
64
+ - `links: [{label, action, style: primary|secondary|link}]` — свои кнопки (до 10): `action` —
65
+ `{kind:"url", href}` (только http(s), mailto:, tel:, относительные и `#якорь`; `javascript:`/`data:`/`vbscript:` бэкенд отклоняет),
66
+ `{kind:"phone", phone}`, `{kind:"email", email}`, `{kind:"page"|"anchor"|"popup"|"bot", …}`;
67
+ - `colors: "brand"` (цвета мессенджеров) | `"theme"` (основной цвет темы).
68
+
69
+ Блок ставится и на обычный сайт: `add_block {container, type: "messengers"}`.
70
+
52
71
  ## Папки, дизайны, шаблоны
53
72
 
54
73
  - Папки страниц (для навигации в кабинете): `add_folder {name}` → id в `results`; `rename_folder {folderId, name}`,
package/src/index.mjs CHANGED
@@ -27,7 +27,11 @@ 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.14.0";
30
+ const VERSION = "0.15.1";
31
+ /** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
32
+ const SITE_INITS = ["starter", "blank", "mini-landing"];
33
+ /** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
34
+ const SITE_NOOP_OPS = [{ op: "set_settings", settings: {} }];
31
35
  const PKG_NAME = "@retensy/mcp";
32
36
  const BASE = (process.env.RETENSY_BASE_URL || "https://bots.retensy.com").replace(/\/+$/, "");
33
37
  const CONFIG_DIR = path.join(os.homedir(), ".retensy-bot-graph");
@@ -542,8 +546,22 @@ function qs(params) {
542
546
  // ============================================================================
543
547
  // Подключения сервисов
544
548
  // ============================================================================
545
- const CONNECT_PAGE = `${BASE}/bots/connect`;
546
- const INTEGRATIONS_PAGE = `${BASE}/bots/integrations`;
549
+ /** IA v2: каталог и подключения — раздел «Интеграции». Старые /bots/connect|integrations остаются в кабинете (Instagram — там). */
550
+ const CONNECT_PAGE = `${BASE}/integrations`;
551
+ const INTEGRATIONS_PAGE = CONNECT_PAGE;
552
+ const LEGACY_CONNECT_PAGE = `${BASE}/bots/connect`;
553
+ /** Ключи с секретами: бэкенд их не отдаёт (только маска hint), но вычищаем и здесь — секрет не должен попасть в вывод. */
554
+ const SECRET_OUT_RX = /^(creds|credsEnc|credentials|password|api_?key|.*secret(key)?|.*token)$/i;
555
+ function withoutSecrets(v) {
556
+ if (Array.isArray(v)) return v.map(withoutSecrets);
557
+ if (!v || typeof v !== "object") return v;
558
+ const out = {};
559
+ for (const [k, x] of Object.entries(v)) {
560
+ if (SECRET_OUT_RX.test(k) && typeof x !== "boolean") continue; // secret:true в схеме каталога — флаг, не секрет
561
+ out[k] = withoutSecrets(x);
562
+ }
563
+ return out;
564
+ }
547
565
  /** Поля кредов — как форма кабинета (IntegrationsPage.tsx PROVIDER_FIELDS). */
548
566
  const PROVIDER_FIELDS = {
549
567
  AMOCRM: { name: "amoCRM", fields: { subdomain: "поддомен: acme из acme.amocrm.ru", longToken: "долгосрочный токен: amoCRM → Интеграции → ваша интеграция → Ключи и доступы" } },
@@ -563,7 +581,7 @@ const normProvider = (p) => PROVIDER_ALIASES[String(p || "").trim().toLowerCase(
563
581
 
564
582
  /** Instagram подключается только входом через Facebook (OAuth) и сейчас выключен в сервисе (instagram.enabled). */
565
583
  function instagramAnswer() {
566
- return linkResult("Instagram: подключение через вход Facebook (OAuth) — сейчас выключено в сервисе", CONNECT_PAGE,
584
+ return linkResult("Instagram: подключение через вход Facebook (OAuth) — сейчас выключено в сервисе", LEGACY_CONNECT_PAGE,
567
585
  "Instagram-аккаунт нельзя подключить по API или токену: только входом через Facebook в кабинете. Сейчас подключение Instagram в retensy выключено (страница /bots/instagram ведёт на список ботов). Открой каталог подключений по ссылке — когда Instagram включат, он появится там. Пока доступны Telegram и MAX (create_bot).");
568
586
  }
569
587
 
@@ -573,6 +591,10 @@ const TOOLS = [
573
591
  { name: "list_bots", description: "Список ботов пользователя (id, имя, статус).", inputSchema: { type: "object", properties: {} } },
574
592
  { name: "list_graphs", description: "Список сценариев САМОГО бота (без узлов). Вебхук-сценарии, которые лишь отвечают через этого бота, сюда не входят — их публикуют в вебе, в «Сценариях» автора.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
575
593
  { name: "list_channels", description: "Список каналов/групп, подключённых к боту (chatId, title, type, статус бота, дата). chatId — числовой id для условия SUBSCRIBED («Подписан на канал»).", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
594
+ { name: "integration_catalog", description: "Каталог сервисов Integration Core (GET /api/integrations/catalog): [{provider, name, category, authType, configSchema: [{key, label, hint, secret}] — поля подключения, actions: [{kind, label, inputs}] — действия для сценария и coreDelivery, healthCheck — умеет ли integration_test}]. 404 — Integration Core выключен в сервисе. Read-only.", inputSchema: { type: "object", properties: {} } },
595
+ { name: "integration_status", description: "Статус подключения (GET /api/integrations/{id}/status): {status: UNKNOWN | OK | NEEDS_REAUTH (ключ отозван/устарел — обнови через connect_integration) | ERROR, lastCheckedAt, lastError, supported}. Только своё подключение (чужое — 403). Read-only, внешний сервис не вызывает.", inputSchema: { type: "object", properties: { connectionId: { type: "string", description: "id из list_integrations" } }, required: ["connectionId"] } },
596
+ { name: "integration_test", description: "Проверить подключение (POST /api/integrations/{id}/test): выполняет ЖИВУЮ проверку ключа во внешнем сервисе от имени владельца (без побочных эффектов — ничего не создаёт) и обновляет статус. Ответ как у integration_status; supported:false — сервис проверку не умеет. Только своё подключение (чужое — 403); 404 — Integration Core выключен.", inputSchema: { type: "object", properties: { connectionId: { type: "string", description: "id из list_integrations" } }, required: ["connectionId"] } },
597
+ { name: "channel_post", description: "Разовый пост от имени бота в канал/группу Telegram или MAX (раздел «Публикации», POST /api/bots/{botId}/linked-chats/{chatId}/post). Бот должен быть администратором канала (list_channels). text и/или mediaUrl — файл из upload_file (тип фото/видео/документ берётся из файла; чужие ссылки не принимаются). Лимиты: Telegram — 4096 символов текста, 1024 подписи к файлу; MAX — 4000. Ответ {ok, messageId?}. Публикует сразу — подтверди текст с пользователем.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "number", description: "chatId из list_channels" }, text: { type: "string" }, mediaUrl: { type: "string", description: "url из upload_file" } }, required: ["botId", "chatId"] } },
576
598
  { name: "list_integrations", description: "Список подключённых сервисов пользователя (GET /api/bots/integrations): {id, provider, title, hint, createdAt}. **id отсюда — это `connectionId`**, обязательное поле действий amocrm_send/amocrm_update/bitrix24_call/getcourse_send/getcourse_order/yametrika_event. Без него действие упадёт «не выбрано подключение». Креды не отдаются — только маскированный hint. Подключить новый — connect_integration. Read-only.", inputSchema: { type: "object", properties: {} } },
577
599
  { name: "get_graph", description: "Получить граф по graphId. Для БОЛЬШИХ графов (десятки узлов JSON может превысить лимит токенов) используй summary:true (компактная сводка: id/type/title/позиции + рёбра) или saveToFile (записать полный граф на диск и вернуть сводку+путь — потом правь файл и заливай через update_graph/edit_graph_live с graphFile).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, summary: { type: "boolean", description: "true = вернуть компактную сводку без объёмных text/cards/buttons" }, saveToFile: { type: "string", description: "Путь: записать полный граф (JSON) на диск, вернуть сводку + путь" } }, required: ["graphId"] } },
578
600
  { name: "create_graph", description: "Создать пустой граф (DRAFT) в боте. Возвращает граф с id.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
@@ -596,7 +618,7 @@ const TOOLS = [
596
618
  { name: "list_bot_users", description: "Пользователи (подписчики/лиды) бота, постранично (GET /api/bots/{botId}/users). Опц. page (с 0), size (по умолч. 25), query (поиск по имени/username/id). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, page: { type: "number" }, size: { type: "number" }, query: { type: "string" } }, required: ["botId"] } },
597
619
  { name: "list_links", description: "Стартовые (трекинговые) ссылки бота с UTM (GET /api/bots/{botId}/links): code, метки, число стартов. Это точки входа в воронку. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
598
620
  { name: "site_list", description: "Сайты пользователя (раздел «Страницы», GET /api/bots/pages): id, title, mode (BLOCKS — сайт из блоков, CODE — файлы/Mini App), url (основной адрес), publishedRevision. Read-only.", inputSchema: { type: "object", properties: {} } },
599
- { name: "site_create", description: "Создать сайт из блоков (POST /api/bots/pages, mode=BLOCKS). Возвращает id. Дальше: site_edit (init=starter — стартовый лендинг, init=blank — пустая главная) → site_publish. slug — «название» в адресе pages.retensy.com/<id>/<slug>/ (необязательно, по умолчанию транслит title).", inputSchema: { type: "object", properties: { title: { type: "string" }, slug: { type: "string" } }, required: ["title"] } },
621
+ { name: "site_create", description: "Создать сайт из блоков (POST /api/bots/pages, mode=BLOCKS). Возвращает id. Дальше: site_edit (init=starter — стартовый лендинг, init=blank — пустая главная, init=mini-landing — мини-лендинг с кнопками мессенджеров) → site_publish. template (starter|blank|mini-landing) — сразу создать черновик из шаблона (как «Мини-лендинг» в редакторе), в ответе revision. slug — «название» в адресе pages.retensy.com/<id>/<slug>/ (необязательно, по умолчанию транслит title).", inputSchema: { type: "object", properties: { title: { type: "string" }, slug: { type: "string" }, template: { type: "string", enum: ["starter", "blank", "mini-landing"] } }, required: ["title"] } },
600
622
  { name: "site_get", description: "Модель сайта из блоков (GET /api/bots/pages/{siteId}/document): revision, draft (SiteModel: theme, globals.header/footer, pages[].blocks[], popups[]) — id страниц/блоков/попапов нужны для site_edit. draft=null — сайт пуст (первый site_edit создаст его). saveToFile — записать модель на диск и вернуть путь.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, saveToFile: { type: "string" } }, required: ["siteId"] } },
601
623
  { name: "site_schema", description: "JSON Schema модели сайта (model) и операций правки (ops) — какие блоки и поля бывают (GET /api/bots/pages/schema). Читай перед первой правкой.", inputSchema: { type: "object", properties: {} } },
602
624
  { name: "site_edit", description: "Правка сайта операциями — всё или ничего (POST /api/bots/pages/{siteId}/document/ops). " +
@@ -608,14 +630,14 @@ const TOOLS = [
608
630
  "Код блока: get_block_code{blockId} (results[i].code: Zero — разметка <zero>…</zero>, остальные — JSON) · set_block_code{blockId,code} · add_block_code{container,code,after?}. " +
609
631
  "Zero-блок (type zero, свободная вёрстка как в Tilda): add_element{blockId, kind: text|image|button|shape|video|html|group, frame?{d:{x,y,w,h,container?,axisX?,axisY?}, t?, m?}, props?, style?, hover?, anim?, link?, parent?, name?, fixed?} · update_element{blockId,elementId, …те же поля, hidden?, locked?, link:null — убрать} · remove_element · move_element{delta: +1 — слой выше} · group_elements{blockId,elementIds[],name?} · ungroup_element. " +
610
632
  "Сайт: set_global{slot: header|footer, on} · set_theme{theme} · set_settings{settings} · add_popup{name} · update_popup{popupId,name?,width?} · remove_popup{popupId}. " +
611
- "props/style/theme/frame — JSON Merge Patch (null удаляет ключ, массивы заменяются целиком). Типы блоков: header, cover, text, image, gallery, buttons, features, form, video, html, spacer, footer, zero. " +
612
- "Значения по экранам: {d, t?, m?} (десктоп/планшет/телефон). revision — защита от перезаписи (409, если сайт изменили); init (starter|blank) — с чего начать пустой сайт. " +
613
- "Ответ: новая revision и results[] с id созданного (и code у get_block_code). Ошибки — HTTP 422 с путями. Тариф: HTML-блок и HTML-элемент Zero публикуются только на платном тарифе (422 при site_publish).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, ops: { type: "array", items: { type: "object" } }, revision: { type: "number" }, init: { type: "string", enum: ["starter", "blank"] } }, required: ["siteId", "ops"] } },
633
+ "props/style/theme/frame — JSON Merge Patch (null удаляет ключ, массивы заменяются целиком). Типы блоков: header, cover, text, image, gallery, buttons, features, form, video, html, spacer, footer, zero, messengers. " +
634
+ "Значения по экранам: {d, t?, m?} (десктоп/планшет/телефон). revision — защита от перезаписи (409, если сайт изменили); init (starter|blank|mini-landing) — с чего начать пустой сайт (на сайте с черновиком игнорируется); только init без ops — создать черновик из шаблона. " +
635
+ "Ответ: новая revision и results[] с id созданного (и code у get_block_code). Ошибки — HTTP 422 с путями. Тариф: HTML-блок и HTML-элемент Zero публикуются только на платном тарифе (422 при site_publish).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, ops: { type: "array", items: { type: "object" } }, revision: { type: "number" }, init: { type: "string", enum: ["starter", "blank", "mini-landing"] } }, required: ["siteId"] } },
614
636
  { name: "site_publish", description: "Опубликовать черновик сайта (POST /api/bots/pages/{siteId}/publish): рендер в статику, адрес начинает отдавать новую версию. Ошибки проверки — HTTP 422 с путями. Возвращает publishedRevision и url.", inputSchema: { type: "object", properties: { siteId: { type: "string" } }, required: ["siteId"] } },
615
637
  { name: "site_upload_asset", description: "Загрузить картинку/видео в сайт (POST /api/bots/pages/{siteId}/upload, папка assets). Передай path (локальный файл) ИЛИ url. Возвращает asset — строку вида assets/<имя> для полей image/logo/icon/style.bg.image.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, path: { type: "string" }, url: { type: "string" } }, required: ["siteId"] } },
616
638
  { name: "site_rollback", description: "Вернуть прошлую публикацию сайта (POST /api/bots/pages/{siteId}/publish/rollback): revision — номер из истории публикаций (site_get → versions[]). Черновик заменяется этой версией и сразу публикуется. Возвращает publishedRevision и url.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, revision: { type: "number" } }, required: ["siteId", "revision"] } },
617
639
  { name: "site_domains", description: "Свои домены сайта (/api/bots/pages/{siteId}/domains). action: list — домены, статусы и dnsTarget (IP для A-записи); add {host, withWww?} — привязать (withWww у корневого домена добавляет www-пару); check {domainId} — перепроверить DNS и сертификат; remove {domainId} — отвязать. Число доменов ограничено тарифом (HTTP 402 с upgradeUrl). Каждое действие возвращает актуальный список.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, action: { type: "string", enum: ["list", "add", "check", "remove"] }, host: { type: "string" }, withWww: { type: "boolean" }, domainId: { type: "string" } }, required: ["siteId", "action"] } },
618
- { name: "site_lead_settings", description: "Куда доставлять заявки из форм сайта (/api/bots/pages/{siteId}/lead-settings). Без settings — прочитать: текущие настройки и доступные вебхук-сценарии и подключения amoCRM. С settings — сохранить целиком: {notifyBot: в бот уведомлений из профиля, notifyEmail: письмо на почту аккаунта, webhookUrl?: POST JSON на ваш адрес, scenarioId?: вебхук-сценарий, который запускает заявка, amoConnectionId?: сделка в amoCRM}.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, settings: { type: "object" } }, required: ["siteId"] } },
640
+ { name: "site_lead_settings", description: "Куда доставлять заявки из форм сайта (/api/bots/pages/{siteId}/lead-settings). Без settings — прочитать: {settings, scenarios (вебхук-сценарии), amoConnections, coreConnections: [{id, name, provider}] — подключения для доставки «Интеграция», без кредов}. С settings — сохранить ЦЕЛИКОМ (сначала прочитай и поменяй нужное): {notifyBot: в бот уведомлений из профиля, notifyEmail: письмо на почту аккаунта, webhookUrl?: POST JSON на ваш адрес, scenarioId?: вебхук-сценарий, который запускает заявка, amoConnectionId?: сделка в amoCRM, coreDelivery?: {connectionId: id из coreConnections, kind: действие сервиса из integration_catalog (actions[].kind, например amocrm_send), params?: {поле действия: шаблон}} | null}. Пустые params заполнятся из заявки (имя, телефон, почта, текст); в шаблонах — {{var.name}}, {{var.phone}}, {{var.email}}, {{var.<имя поля>}}, {{var.lead_text}} (текст заявки). Чужое подключение или неизвестный kind — 400. Запуск сценариев по заявке — триггер TRIGGER_SITE_FORM (скилл build-bot-funnel).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, settings: { type: "object", properties: { notifyBot: { type: "boolean" }, notifyEmail: { type: "boolean" }, webhookUrl: { type: "string" }, scenarioId: { type: "string" }, amoConnectionId: { type: "string" }, coreDelivery: { type: ["object", "null"], properties: { connectionId: { type: "string" }, kind: { type: "string" }, params: { type: "object", additionalProperties: { type: "string" } } } } } } }, required: ["siteId"] } },
619
641
  { name: "site_leads", description: "Заявки из форм сайта (GET /api/bots/pages/{siteId}/leads): поля, UTM, статус доставки. page (с 0), size (до 100). Read-only.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["siteId"] } },
620
642
  { name: "article_list", description: "Список СВОИХ статей блога retensy (GET /api/articles/my): id, slug, title, viewCount, даты. id нужен для article_update, slug — публичный адрес /articles/{slug}. Read-only.", inputSchema: { type: "object", properties: {} } },
621
643
  { name: "article_get", description: "Получить статью блога по slug (GET /api/articles/by-slug/{slug}) — публичное чтение, в т.ч. чужие. Возвращает title, content (Markdown), excerpt, coverImage, viewCount.", inputSchema: { type: "object", properties: { slug: { type: "string", description: "slug статьи (часть адреса /articles/{slug})" } }, required: ["slug"] } },
@@ -626,7 +648,7 @@ const TOOLS = [
626
648
  { name: "bot_stop", description: "Остановить бота (POST /api/bots/{botId}/stop): снимает вебхук, бот перестаёт отвечать, сценарии и подписчики сохраняются. Запуск обратно — bot_resume.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
627
649
  { name: "bot_resume", description: "Запустить остановленного бота или бота, приостановленного лимитом тарифа (POST /api/bots/{botId}/resume). Если лимит ботов тарифа исчерпан — HTTP 402 со ссылкой на смену тарифа.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
628
650
  // ---- Подключения ----
629
- { name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Креды не возвращаются и не попадают в отчёты.", inputSchema: { type: "object", properties: { provider: { type: "string", description: "AMOCRM | BITRIX24 | GETCOURSE | YAMETRIKA | YOOKASSA | GOOGLE_SHEETS | INSTAGRAM" }, title: { type: "string", description: "Название подключения в кабинете (например «amoCRM продажи»)" }, creds: { type: "object", description: "Поля провайдера, см. описание" }, connectionId: { type: "string", description: "id существующего подключения (list_integrations) — обновить его" } }, required: ["provider"] } },
651
+ { name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Креды хранятся в сервисе зашифрованными и НИКОГДА не возвращаются — ни здесь, ни в list_integrations (только маска hint); в отчёты не попадают. Проверить ключ после подключения — integration_test.", inputSchema: { type: "object", properties: { provider: { type: "string", description: "AMOCRM | BITRIX24 | GETCOURSE | YAMETRIKA | YOOKASSA | GOOGLE_SHEETS | INSTAGRAM" }, title: { type: "string", description: "Название подключения в кабинете (например «amoCRM продажи»)" }, creds: { type: "object", description: "Поля провайдера, см. описание" }, connectionId: { type: "string", description: "id существующего подключения (list_integrations) — обновить его" } }, required: ["provider"] } },
630
652
  { name: "disconnect_integration", description: "Удалить подключение сервиса по id из list_integrations (DELETE /api/bots/integrations/{id}). Действия сценария с этим connectionId перестанут работать.", inputSchema: { type: "object", properties: { connectionId: { type: "string" } }, required: ["connectionId"] } },
631
653
  // ---- Рассылки ----
632
654
  { name: "broadcast_list", description: "Рассылки. Без botId — по всем ботам постранично (GET /api/bots/broadcasts): {counts: {drafts, scheduled, sent, recurring}, page: {content: [{id, botId, botUsername, name, status, direct, totalJobs, sentJobs, failedJobs, skippedByQuota, scheduledAt, createdAt}], totalElements…}}; group: scheduled (ещё не начали) | sent (идут/завершены). С botId — полная история одного бота. status: EXPANDING/MATERIALIZING/READY (ждёт) → RUNNING → DONE | CANCELLING → CANCELLED | FAILED. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, group: { type: "string", enum: ["scheduled", "sent"] }, page: { type: "number" }, size: { type: "number", description: "до 100, по умолчанию 20" } } } },
@@ -677,7 +699,26 @@ async function handleCall(params) {
677
699
  case "list_bots": return okResult(await api("/api/bots"));
678
700
  case "list_graphs": return okResult(await api(`/api/bots/${a.botId}/graphs`));
679
701
  case "list_channels": return okResult(await api(`/api/bots/${a.botId}/linked-chats`));
680
- case "list_integrations": return okResult(await api("/api/bots/integrations"));
702
+ case "list_integrations": return okResult(withoutSecrets(await api("/api/bots/integrations")));
703
+ case "integration_catalog": return okResult(withoutSecrets(await api("/api/integrations/catalog")));
704
+ case "integration_status":
705
+ case "integration_test": {
706
+ const id = String(a.connectionId || "").trim();
707
+ if (!id) throw new Error("Передай connectionId — id подключения из list_integrations.");
708
+ const live = params.name === "integration_test";
709
+ const p_ = `/api/integrations/${encodeURIComponent(id)}/${live ? "test" : "status"}`;
710
+ return okResult(withoutSecrets(await api(p_, live ? { method: "POST" } : undefined)));
711
+ }
712
+ case "channel_post": {
713
+ if (!a.botId) throw new Error("Передай botId (list_bots).");
714
+ const chatId = Number(a.chatId);
715
+ if (!Number.isSafeInteger(chatId)) throw new Error("chatId — числовой id канала из list_channels (например -1001234567890).");
716
+ const text = a.text == null ? "" : String(a.text);
717
+ if (!text.trim() && !a.mediaUrl) throw new Error("Пустой пост: передай text и/или mediaUrl (файл из upload_file).");
718
+ const body = { text };
719
+ if (a.mediaUrl) body.mediaUrl = String(a.mediaUrl);
720
+ return okResult(await api(`/api/bots/${encodeURIComponent(a.botId)}/linked-chats/${chatId}/post`, { method: "POST", body }));
721
+ }
681
722
  case "get_graph": {
682
723
  const g = await api(`/api/bots/graphs/${a.graphId}`);
683
724
  if (a.saveToFile) {
@@ -845,7 +886,12 @@ async function handleCall(params) {
845
886
  case "site_list": return okResult(await api("/api/bots/pages"));
846
887
  case "site_create": {
847
888
  if (!a.title) throw new Error("Передай title сайта.");
848
- return okResult(await api("/api/bots/pages", { method: "POST", body: { title: a.title, slug: a.slug || undefined, mode: "BLOCKS" } }));
889
+ if (a.template != null && !SITE_INITS.includes(a.template)) throw new Error(`template — одно из: ${SITE_INITS.join(", ")}.`);
890
+ const page = await api("/api/bots/pages", { method: "POST", body: { title: a.title, slug: a.slug || undefined, mode: "BLOCKS" } });
891
+ if (!a.template) return okResult(page);
892
+ // черновик сразу, как редактор при «Мини-лендинг»: бэкенд не берёт пустой ops[] — безвредная set_settings{}
893
+ const doc = await api(`/api/bots/pages/${page.id}/document/ops`, { method: "POST", body: { ops: SITE_NOOP_OPS, init: a.template } });
894
+ return okResult({ ...page, template: a.template, revision: doc?.revision });
849
895
  }
850
896
  case "site_get": {
851
897
  const doc = await api(`/api/bots/pages/${a.siteId}/document`);
@@ -858,8 +904,11 @@ async function handleCall(params) {
858
904
  }
859
905
  case "site_schema": return okResult(await api("/api/bots/pages/schema"));
860
906
  case "site_edit": {
861
- if (!Array.isArray(a.ops) || !a.ops.length) throw new Error("Передай ops — массив операций (см. site_schema).");
862
- return okResult(await api(`/api/bots/pages/${a.siteId}/document/ops`, { method: "POST", body: { ops: a.ops, revision: a.revision, init: a.init } }));
907
+ const hasOps = Array.isArray(a.ops) && a.ops.length > 0;
908
+ if (!hasOps && !a.init) throw new Error("Передай ops — массив операций (см. site_schema) — или init, чтобы только создать черновик.");
909
+ if (a.init != null && !SITE_INITS.includes(a.init)) throw new Error(`init — одно из: ${SITE_INITS.join(", ")}.`);
910
+ const ops = hasOps ? a.ops : SITE_NOOP_OPS; // бэкенд отвечает 422 на пустой ops[]
911
+ return okResult(await api(`/api/bots/pages/${a.siteId}/document/ops`, { method: "POST", body: { ops, revision: a.revision, init: a.init } }));
863
912
  }
864
913
  case "site_publish": {
865
914
  const r = await api(`/api/bots/pages/${a.siteId}/publish`, { method: "POST" });
@@ -891,8 +940,8 @@ async function handleCall(params) {
891
940
  }
892
941
  case "site_lead_settings": {
893
942
  const p_ = `/api/bots/pages/${a.siteId}/lead-settings`;
894
- if (a.settings == null) return okResult(await api(p_));
895
- return okResult(await api(p_, { method: "PUT", body: a.settings }));
943
+ if (a.settings == null) return okResult(withoutSecrets(await api(p_)));
944
+ return okResult(withoutSecrets(await api(p_, { method: "PUT", body: a.settings })));
896
945
  }
897
946
  case "article_list": return okResult(await api("/api/articles/my"));
898
947
  case "article_get": return okResult(await api(`/api/articles/by-slug/${encodeURIComponent(a.slug)}`));
@@ -968,7 +1017,7 @@ async function handleCall(params) {
968
1017
  const saved = a.connectionId
969
1018
  ? await api(`/api/bots/integrations/${a.connectionId}`, { method: "PUT", body: { title: a.title, creds: Object.keys(creds).length ? creds : undefined } })
970
1019
  : await api("/api/bots/integrations", { method: "POST", body: { provider, title, creds } });
971
- return okResult({ connected: true, connectionId: saved?.id, ...saved, hint: "connectionId ставь в действия сценария (amocrm_send, bitrix24_call, getcourse_send, yametrika_event, оплата ЮKassa) и в site_lead_settings.amoConnectionId." });
1020
+ return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (amocrm_send, bitrix24_call, getcourse_send, yametrika_event, оплата ЮKassa) и в site_lead_settings (amoConnectionId или coreDelivery.connectionId). Проверить ключ — integration_test." });
972
1021
  } catch (e) {
973
1022
  if (e.status === 400) throw new Error(`${spec.name} не подключён: ${bodyReason(e.data) || "креды не приняты"}. Проверь поля: ${Object.entries(spec.fields).map(([k, v]) => `${k} — ${v}`).join("; ")}.`);
974
1023
  throw e;