@retensy/mcp 0.19.0 → 0.21.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.
@@ -1,105 +1,105 @@
1
- ---
2
- name: build-site
3
- description: Собрать сайт или лендинг из блоков в сервисе retensy (раздел «Страницы») — обложка, тексты, преимущества, галерея, форма заявки, попап, несколько страниц — и опубликовать. Использовать, когда пользователь просит «сделать сайт», «лендинг», «страницу с формой заявки», «сайт для бизнеса».
4
- ---
5
-
6
- # Сайт из блоков (retensy «Страницы»)
7
-
8
- Сайт — JSON-модель: тема, общие шапка и подвал, страницы с блоками, попапы. Правится операциями `site_edit`
9
- (всё или ничего, с проверкой схемы на сервере), публикуется `site_publish`. Тот же сайт пользователь потом правит
10
- мышкой в кабинете — модель общая.
11
-
12
- ## Порядок
13
-
14
- 1. `site_schema` — какие блоки и поля бывают (`model`) и какие операции есть (`ops`); `site_templates` — готовые
15
- секции из библиотеки.
16
- 2. `site_create {title}` → `id`. Для правки существующего — `site_list`, `site_get {siteId}`.
17
- 3. Первый `site_edit` с `init`: `starter` — готовый лендинг (шапка, обложка, текст, преимущества, форма, подвал),
18
- `blank` — пустая главная или `mini-landing` — мини-лендинг «как Taplink» (см. ниже). Только `init` без `ops` —
19
- создать черновик из шаблона; то же сразу при создании — `site_create {title, template}`. На сайте с черновиком
20
- `init` игнорируется. Заполни тексты, не выдумывай факты о бизнесе — спрашивай.
21
- 4. Картинки — `site_upload_asset {siteId, path|url}` → `assets/…` в поля `image`, `logo`, `icon`, `style.bg.image`.
22
- 5. `site_get` — проверь модель, `site_publish` — сайт открыт по `url`.
23
- 6. После публикации: заявки — `site_leads`, куда их слать — `site_lead_settings` (бот уведомлений, почта, вебхук,
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`
27
- → `check`); неудачная публикация — `site_rollback {revision}` из `site_get → versions[]`.
28
-
29
- ## Правила модели
30
-
31
- - Значения по экранам — `{d, t?, m?}` (десктоп ≥1024, планшет 640–1023, телефон <640); без `t`/`m` берётся больший.
32
- - `props`, `style`, `theme` в операциях — JSON Merge Patch: объекты сливаются, `null` удаляет ключ, массивы
33
- (кнопки, ссылки, поля формы, фото) передаются целиком.
34
- - Ссылки (`action`): `{kind:"url", href}` (https/tel/mailto/tg/#якорь), `{kind:"page", pageId}`,
35
- `{kind:"anchor", blockId}`, `{kind:"popup", popupId}`.
36
- - Текст с разметкой (Rich): только `<b> <i> <u> <s> <br> <a href> <span style="color:#…">`.
37
- - Цвета — `#rrggbb`. Шрифты: inter, montserrat, roboto, pt-sans, pt-serif, rubik, oswald.
38
- - `revision` из ответа передавай в следующий `site_edit` — сервер не даст перезаписать правки из кабинета (409).
39
-
40
- ## Zero-блок — свободная вёрстка (как Zero Block в Tilda)
41
-
42
- Блок `type: "zero"` — артборд, на котором элементы стоят по координатам. Для дизайнерских экранов, где готовых
43
- блоков мало.
44
-
45
- - Добавить: `add_block {container, type: "zero", props: {height: {d: 600, m: 720}}}` → id блока в `results`.
46
- - Элементы: `add_element {blockId, kind, frame, props, style?, link?, anim?}`, `kind`: `text`, `image`, `button`,
47
- `shape`, `video`, `html`, `group`. `frame` — по экранам: `{d: {x, y, w, h}, m?: {…}}`; `h: "auto"` у текста — высота
48
- по содержимому. Нет `t`/`m` — кадр выводится из большего экрана в масштабе; задай `m`, если на телефоне нужна
49
- другая раскладка. `container: "window"` + `axisX/axisY` — привязка к краям окна, а не к сетке.
50
- - Правка — `update_element` (Merge Patch), порядок слоёв — `move_element {delta}`, группы — `group_elements` /
51
- `ungroup_element`; у элемента в группе кадр — относительно группы (`parent`).
52
- - Целиком переписать блок проще кодом: `get_block_code {blockId}` → в `results[i].code` разметка `<zero …>…</zero>`,
53
- поправь и верни `set_block_code {blockId, code}`; новый блок из кода — `add_block_code {container, code}`. У
54
- обычных блоков код — JSON `{type, variant, props, style}`. Тот же код пользователь видит во вкладке «Код» редактора.
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
-
71
- ## Папки, дизайны, шаблоны
72
-
73
- - Папки страниц (для навигации в кабинете): `add_folder {name}` → id в `results`; `rename_folder {folderId, name}`,
74
- `remove_folder {folderId}`. Страницу в папку — `update_page {pageId, patch: {folder: folderId}}`.
75
- - Дизайны — отдельные экраны-макеты из Zero-кадров (не страницы сайта): `add_design {name}` → id;
76
- `update_design {designId, name}`, `remove_design {designId}`. Кадр: `add_design_frame {designId, name, w, h}` →
77
- в `results` `blockId` Zero-кадра — дальше на нём работают `add_element`/`update_element`/`get_block_code` и др.
78
- - Шаблоны: `site_templates` → `{categories, templates: [{id, category, title, description?, blocks}]}` (`blocks` —
79
- сколько блоков вставится). Вставка — `add_template {container, templateId, after?}`: блоки шаблона
80
- встают в страницу/попап, дальше правь их тексты обычными `update_block`. Быстрее, чем собирать блоки с нуля.
81
-
82
- ## Тариф
83
-
84
- - HTML-блок и HTML-элемент Zero публикуются только на платном тарифе: на бесплатном `site_publish` вернёт 422 с
85
- путями этих элементов — замени их обычными блоками или предложи тариф.
86
- - Число своих доменов ограничено тарифом (`site_domains add` → 402 со ссылкой на смену тарифа — передай её
87
- пользователю, оплата только в браузере); www-пара корневого домена не в счёт.
88
- - На бесплатном тарифе адрес на pages.retensy.com закрыт от поисковиков (noindex).
89
-
90
- ## Пример: лендинг кофейни с попапом заявки
91
-
92
- ```json
93
- [
94
- {"op": "set_theme", "theme": {"colors": {"primary": "#b5651d"}, "fonts": {"heading": "pt-serif", "body": "inter"}}},
95
- {"op": "add_popup", "name": "Бронь столика"},
96
- {"op": "add_block", "container": "<id главной>", "type": "cover", "after": -1,
97
- "props": {"title": "Кофе, ради которого приходят", "subtitle": "Обжариваем сами, с 8:00 до 22:00",
98
- "buttons": [{"label": "Забронировать столик", "style": "primary", "action": {"kind": "popup", "popupId": "<id из results>"}}],
99
- "height": {"d": "screen", "m": "auto"}},
100
- "style": {"bg": {"image": "assets/cafe-x1y2.jpg", "overlay": 0.5}, "textColor": "#ffffff"}}
101
- ]
102
- ```
103
-
104
- Порядок важен: id созданного попапа приходит в `results` — если он нужен в той же правке, сделай два вызова
105
- `site_edit` (сначала `add_popup`, потом блок с кнопкой).
1
+ ---
2
+ name: build-site
3
+ description: Собрать сайт или лендинг из блоков в сервисе retensy (раздел «Страницы») — обложка, тексты, преимущества, галерея, форма заявки, попап, несколько страниц — и опубликовать. Использовать, когда пользователь просит «сделать сайт», «лендинг», «страницу с формой заявки», «сайт для бизнеса».
4
+ ---
5
+
6
+ # Сайт из блоков (retensy «Страницы»)
7
+
8
+ Сайт — JSON-модель: тема, общие шапка и подвал, страницы с блоками, попапы. Правится операциями `site_edit`
9
+ (всё или ничего, с проверкой схемы на сервере), публикуется `site_publish`. Тот же сайт пользователь потом правит
10
+ мышкой в кабинете — модель общая.
11
+
12
+ ## Порядок
13
+
14
+ 1. `site_schema` — какие блоки и поля бывают (`model`) и какие операции есть (`ops`); `site_templates` — готовые
15
+ секции из библиотеки.
16
+ 2. `site_create {title}` → `id`. Для правки существующего — `site_list`, `site_get {siteId}`.
17
+ 3. Первый `site_edit` с `init`: `starter` — готовый лендинг (шапка, обложка, текст, преимущества, форма, подвал),
18
+ `blank` — пустая главная или `mini-landing` — мини-лендинг «как Taplink» (см. ниже). Только `init` без `ops` —
19
+ создать черновик из шаблона; то же сразу при создании — `site_create {title, template}`. На сайте с черновиком
20
+ `init` игнорируется. Заполни тексты, не выдумывай факты о бизнесе — спрашивай.
21
+ 4. Картинки — `site_upload_asset {siteId, path|url}` → `assets/…` в поля `image`, `logo`, `icon`, `style.bg.image`.
22
+ 5. `site_get` — проверь модель, `site_publish` — сайт открыт по `url`.
23
+ 6. После публикации: заявки — `site_leads`, куда их слать — `site_lead_settings` (бот уведомлений, почта, вебхук,
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`
27
+ → `check`); неудачная публикация — `site_rollback {revision}` из `site_get → versions[]`.
28
+
29
+ ## Правила модели
30
+
31
+ - Значения по экранам — `{d, t?, m?}` (десктоп ≥1024, планшет 640–1023, телефон <640); без `t`/`m` берётся больший.
32
+ - `props`, `style`, `theme` в операциях — JSON Merge Patch: объекты сливаются, `null` удаляет ключ, массивы
33
+ (кнопки, ссылки, поля формы, фото) передаются целиком.
34
+ - Ссылки (`action`): `{kind:"url", href}` (https/tel/mailto/tg/#якорь), `{kind:"page", pageId}`,
35
+ `{kind:"anchor", blockId}`, `{kind:"popup", popupId}`.
36
+ - Текст с разметкой (Rich): только `<b> <i> <u> <s> <br> <a href> <span style="color:#…">`.
37
+ - Цвета — `#rrggbb`. Шрифты: inter, montserrat, roboto, pt-sans, pt-serif, rubik, oswald.
38
+ - `revision` из ответа передавай в следующий `site_edit` — сервер не даст перезаписать правки из кабинета (409).
39
+
40
+ ## Zero-блок — свободная вёрстка (как Zero Block в Tilda)
41
+
42
+ Блок `type: "zero"` — артборд, на котором элементы стоят по координатам. Для дизайнерских экранов, где готовых
43
+ блоков мало.
44
+
45
+ - Добавить: `add_block {container, type: "zero", props: {height: {d: 600, m: 720}}}` → id блока в `results`.
46
+ - Элементы: `add_element {blockId, kind, frame, props, style?, link?, anim?}`, `kind`: `text`, `image`, `button`,
47
+ `shape`, `video`, `html`, `group`. `frame` — по экранам: `{d: {x, y, w, h}, m?: {…}}`; `h: "auto"` у текста — высота
48
+ по содержимому. Нет `t`/`m` — кадр выводится из большего экрана в масштабе; задай `m`, если на телефоне нужна
49
+ другая раскладка. `container: "window"` + `axisX/axisY` — привязка к краям окна, а не к сетке.
50
+ - Правка — `update_element` (Merge Patch), порядок слоёв — `move_element {delta}`, группы — `group_elements` /
51
+ `ungroup_element`; у элемента в группе кадр — относительно группы (`parent`).
52
+ - Целиком переписать блок проще кодом: `get_block_code {blockId}` → в `results[i].code` разметка `<zero …>…</zero>`,
53
+ поправь и верни `set_block_code {blockId, code}`; новый блок из кода — `add_block_code {container, code}`. У
54
+ обычных блоков код — JSON `{type, variant, props, style}`. Тот же код пользователь видит во вкладке «Код» редактора.
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
+
71
+ ## Папки, дизайны, шаблоны
72
+
73
+ - Папки страниц (для навигации в кабинете): `add_folder {name}` → id в `results`; `rename_folder {folderId, name}`,
74
+ `remove_folder {folderId}`. Страницу в папку — `update_page {pageId, patch: {folder: folderId}}`.
75
+ - Дизайны — отдельные экраны-макеты из Zero-кадров (не страницы сайта): `add_design {name}` → id;
76
+ `update_design {designId, name}`, `remove_design {designId}`. Кадр: `add_design_frame {designId, name, w, h}` →
77
+ в `results` `blockId` Zero-кадра — дальше на нём работают `add_element`/`update_element`/`get_block_code` и др.
78
+ - Шаблоны: `site_templates` → `{categories, templates: [{id, category, title, description?, blocks}]}` (`blocks` —
79
+ сколько блоков вставится). Вставка — `add_template {container, templateId, after?}`: блоки шаблона
80
+ встают в страницу/попап, дальше правь их тексты обычными `update_block`. Быстрее, чем собирать блоки с нуля.
81
+
82
+ ## Тариф
83
+
84
+ - HTML-блок и HTML-элемент Zero публикуются только на платном тарифе: на бесплатном `site_publish` вернёт 422 с
85
+ путями этих элементов — замени их обычными блоками или предложи тариф.
86
+ - Число своих доменов ограничено тарифом (`site_domains add` → 402 со ссылкой на смену тарифа — передай её
87
+ пользователю, оплата только в браузере); www-пара корневого домена не в счёт.
88
+ - На бесплатном тарифе адрес на pages.retensy.com закрыт от поисковиков (noindex).
89
+
90
+ ## Пример: лендинг кофейни с попапом заявки
91
+
92
+ ```json
93
+ [
94
+ {"op": "set_theme", "theme": {"colors": {"primary": "#b5651d"}, "fonts": {"heading": "pt-serif", "body": "inter"}}},
95
+ {"op": "add_popup", "name": "Бронь столика"},
96
+ {"op": "add_block", "container": "<id главной>", "type": "cover", "after": -1,
97
+ "props": {"title": "Кофе, ради которого приходят", "subtitle": "Обжариваем сами, с 8:00 до 22:00",
98
+ "buttons": [{"label": "Забронировать столик", "style": "primary", "action": {"kind": "popup", "popupId": "<id из results>"}}],
99
+ "height": {"d": "screen", "m": "auto"}},
100
+ "style": {"bg": {"image": "assets/cafe-x1y2.jpg", "overlay": 0.5}, "textColor": "#ffffff"}}
101
+ ]
102
+ ```
103
+
104
+ Порядок важен: id созданного попапа приходит в `results` — если он нужен в той же правке, сделай два вызова
105
+ `site_edit` (сначала `add_popup`, потом блок с кнопкой).
@@ -1,49 +1,49 @@
1
- ---
2
- name: send-broadcast
3
- description: Сделать рассылку подписчикам ботов retensy (Telegram/MAX) — сейчас, по расписанию или повторяющуюся, по нескольким ботам и с фильтром по тегам; черновики, отмена, статистика отправки. Использовать, когда пользователь просит «разослать», «сделать рассылку», «напомнить всем подписчикам», «отправить акцию в бота».
4
- ---
5
-
6
- # Рассылки retensy
7
-
8
- ## Порядок
9
-
10
- 1. `list_bots` → id ботов. У Instagram-ботов рассылок нет (Meta разрешает писать только в окне 24 ч) — предложи
11
- сценарий с триггером.
12
- 2. Собери сообщения (до 5) и уточни у пользователя: кому (теги), когда, по каким ботам. Тексты не выдумывай.
13
- 3. Медиа — `upload_file {path|url}` → `url` в `mediaUrl`/`mediaUrls`.
14
- 4. `broadcast_preview {botIds, tagsAll?, tagsNone?}` — назови пользователю число получателей и **дождись
15
- подтверждения**: рассылку не вернуть, а получатели списываются с месячной квоты тарифа.
16
- 5. `broadcast_send {name, botIds, messages, tagsAll?, tagsNone?, scheduledAt?}` → `{broadcastIds, totalAudience}`.
17
- Не уверен в тексте — сначала `broadcast_drafts {action: "create", …}`: пользователь увидит черновик в кабинете,
18
- отправка — `broadcast_send {draftId}` (черновик после отправки удаляется).
19
- 6. Статус — `broadcast_get {broadcastId}` (с причинами ошибок) / `broadcast_list`; остановить — `broadcast_cancel`.
20
- Повторить или взять за основу — `broadcast_duplicate {broadcastId | draftId}` → черновик «… (копия)».
21
-
22
- ## Сообщение
23
-
24
- `{type, text?, mediaUrl?, mediaUrls?, buttons?}`; строка — короткая запись `TEXT`.
25
-
26
- | type | Обязательно | Текст | Кнопки |
27
- |---|---|---|---|
28
- | `TEXT` | `text` до 4096 | да | до 8 |
29
- | `PHOTO` `VIDEO` `AUDIO` `FILE` `VOICE` | `mediaUrl` | подпись до 1024 | до 8 |
30
- | `VIDEONOTE` (кружок) | `mediaUrl` | нет | до 8 |
31
- | `GALLERY` | `mediaUrls`: 2–10 картинок | подпись до 1024 | нет |
32
-
33
- - Текст — Telegram-HTML: `<b> <i> <u> <s> <code> <pre> <blockquote> <tg-spoiler> <a href="https://…">`.
34
- Перенос строки — `\n`, `<br>` не работает.
35
- - Кнопки — только ссылки `[{text, url}]`. Нужна ветка по нажатию — это рассылка по сценарию: `broadcast_send
36
- {botId, graphId, entryNodeId?, name}` запускает сценарий бота для каждого получателя.
37
-
38
- ## Аудитория и время
39
-
40
- - Подписчики бота; `tagsAll` — есть все эти теги, `tagsNone` — нет ни одного. До 50 000 на бота, до 20 ботов одного
41
- владельца за раз (по каждому боту создаётся своя рассылка).
42
- - `scheduledAt` — ISO 8601; без пояса — московское время. Отложенная считает аудиторию в момент отправки.
43
- - Повтор: `broadcast_recurring {action: "create", name, botIds, messages, recurrence: DAILY|MONTHLY|YEARLY,
44
- firstRunAt}`; список — `action: "list"`, остановить — `action: "stop", ruleId`.
45
-
46
- ## Тариф
47
-
48
- Рассылки — на платном тарифе. HTTP 402 приходит со ссылкой (`upgradeUrl` или `/bots/subscription`): передай её
49
- пользователю — оплата и смена тарифа только в браузере, потом повтори отправку.
1
+ ---
2
+ name: send-broadcast
3
+ description: Сделать рассылку подписчикам ботов retensy (Telegram/MAX) — сейчас, по расписанию или повторяющуюся, по нескольким ботам и с фильтром по тегам; черновики, отмена, статистика отправки. Использовать, когда пользователь просит «разослать», «сделать рассылку», «напомнить всем подписчикам», «отправить акцию в бота».
4
+ ---
5
+
6
+ # Рассылки retensy
7
+
8
+ ## Порядок
9
+
10
+ 1. `list_bots` → id ботов. У Instagram-ботов рассылок нет (Meta разрешает писать только в окне 24 ч) — предложи
11
+ сценарий с триггером.
12
+ 2. Собери сообщения (до 5) и уточни у пользователя: кому (теги), когда, по каким ботам. Тексты не выдумывай.
13
+ 3. Медиа — `upload_file {path|url}` → `url` в `mediaUrl`/`mediaUrls`.
14
+ 4. `broadcast_preview {botIds, tagsAll?, tagsNone?}` — назови пользователю число получателей и **дождись
15
+ подтверждения**: рассылку не вернуть, а получатели списываются с месячной квоты тарифа.
16
+ 5. `broadcast_send {name, botIds, messages, tagsAll?, tagsNone?, scheduledAt?}` → `{broadcastIds, totalAudience}`.
17
+ Не уверен в тексте — сначала `broadcast_drafts {action: "create", …}`: пользователь увидит черновик в кабинете,
18
+ отправка — `broadcast_send {draftId}` (черновик после отправки удаляется).
19
+ 6. Статус — `broadcast_get {broadcastId}` (с причинами ошибок) / `broadcast_list`; остановить — `broadcast_cancel`.
20
+ Повторить или взять за основу — `broadcast_duplicate {broadcastId | draftId}` → черновик «… (копия)».
21
+
22
+ ## Сообщение
23
+
24
+ `{type, text?, mediaUrl?, mediaUrls?, buttons?}`; строка — короткая запись `TEXT`.
25
+
26
+ | type | Обязательно | Текст | Кнопки |
27
+ |---|---|---|---|
28
+ | `TEXT` | `text` до 4096 | да | до 8 |
29
+ | `PHOTO` `VIDEO` `AUDIO` `FILE` `VOICE` | `mediaUrl` | подпись до 1024 | до 8 |
30
+ | `VIDEONOTE` (кружок) | `mediaUrl` | нет | до 8 |
31
+ | `GALLERY` | `mediaUrls`: 2–10 картинок | подпись до 1024 | нет |
32
+
33
+ - Текст — Telegram-HTML: `<b> <i> <u> <s> <code> <pre> <blockquote> <tg-spoiler> <a href="https://…">`.
34
+ Перенос строки — `\n`, `<br>` не работает.
35
+ - Кнопки — только ссылки `[{text, url}]`. Нужна ветка по нажатию — это рассылка по сценарию: `broadcast_send
36
+ {botId, graphId, entryNodeId?, name}` запускает сценарий бота для каждого получателя.
37
+
38
+ ## Аудитория и время
39
+
40
+ - Подписчики бота; `tagsAll` — есть все эти теги, `tagsNone` — нет ни одного. До 50 000 на бота, до 20 ботов одного
41
+ владельца за раз (по каждому боту создаётся своя рассылка).
42
+ - `scheduledAt` — ISO 8601; без пояса — московское время. Отложенная считает аудиторию в момент отправки.
43
+ - Повтор: `broadcast_recurring {action: "create", name, botIds, messages, recurrence: DAILY|MONTHLY|YEARLY,
44
+ firstRunAt}`; список — `action: "list"`, остановить — `action: "stop", ruleId`.
45
+
46
+ ## Тариф
47
+
48
+ Рассылки — на платном тарифе. HTTP 402 приходит со ссылкой (`upgradeUrl` или `/bots/subscription`): передай её
49
+ пользователю — оплата и смена тарифа только в браузере, потом повтори отправку.
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.19.0";
30
+ const VERSION = "0.21.0";
31
31
  /** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
32
32
  const SITE_INITS = ["starter", "blank", "mini-landing"];
33
33
  /** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
@@ -614,6 +614,16 @@ function qs(params) {
614
614
  return parts.length ? `?${parts.join("&")}` : "";
615
615
  }
616
616
 
617
+ // Выгрузка (CSV/JSON): с savePath — в локальный файл (большие выгрузки не тащим в контекст), без — текстом.
618
+ function exportResult(data, savePath) {
619
+ const text = data == null ? "" : typeof data === "string" ? data : JSON.stringify(data, null, 2);
620
+ if (!savePath) return okResult(text || "Пусто: выгружать нечего.");
621
+ const abs = path.resolve(String(savePath).replace(/^~(?=$|[/\\])/, os.homedir()));
622
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
623
+ fs.writeFileSync(abs, text);
624
+ return okResult({ saved: abs, bytes: Buffer.byteLength(text) });
625
+ }
626
+
617
627
  // =====================================================================// Подключения сервисов
618
628
  // =====================================================================/** IA v2: каталог и подключения — раздел «Интеграции». Старые /bots/connect|integrations остаются в кабинете (Instagram — там). */
619
629
  const CONNECT_PAGE = `${BASE}/integrations`;
@@ -671,7 +681,7 @@ const TOOLS = [
671
681
  { name: "edit_graph_live", description: "РЕКОМЕНДОВАННЫЙ способ правки СУЩЕСТВУЮЩЕГО (часто живого/опубликованного) сценария: редактирует ТОТ ЖЕ graphId НА МЕСТЕ (id не меняется) и сначала снимает авто-бэкап текущего состояния в один rolling-граф «🔙 Авто-бэкап». НЕ клонирует и НЕ создаёт новый активный граф. Открытые редакторы перечитают граф вживую (external_update), бот применит изменения сразу (читает активный граф заново из БД). Используй ВМЕСТО clone+publish, когда нужно поправить сценарий, который уже открыт/в проде. ВАЖНО: правку активного графа сервер проверяет как публикацию (валидатор, платные блоки, лимит блоков тарифа, платформа) — при ошибках HTTP 422 со всеми code@nodeId, граф НЕ изменён, бот работает на прежней версии. Прогоняй offline validate.mjs и dry_run заранее, чтобы не ловить 422. Живой граф бота — с isActive:true в list_graphs — для бот-сценария (после publish_graph черновика — publishedGraphId, не id черновика); правка черновика до бота не доходит.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, graph: { type: "object" }, nodes: { type: "array" }, edges: { type: "array" }, canvasMeta: { type: "object" }, name: { type: "string" }, graphFile: { type: "string", description: "Путь к локальному JSON графа (вместо инлайн-передачи); поддерживается ~" }, backup: { type: "boolean", description: "Снимать авто-бэкап предыдущего состояния перед правкой (по умолчанию true)." } }, required: ["graphId"] } },
672
682
  { name: "patch_graph", description: "Точечная правка БОЛЬШОГО/живого графа без отправки графа целиком: сервер сам берёт граф по graphId, делает строковые замены в его JSON, проверяет валидность и заливает обратно НА МЕСТЕ (с авто-бэкапом). Идеально, когда граф слишком велик, чтобы передавать его целиком через update_graph/edit_graph_live — напр. сменить id канала в условиях SUBSCRIBED, ссылки кнопок, тексты. replacements: [{find, replace}] — заменяются ВСЕ вхождения; делай find максимально специфичным, чтобы не задеть лишнее. preview=true — только показать число совпадений, ничего не сохраняя. Бот применит изменения сразу только у опубликованного графа (читает активный граф заново из БД); патч черновика до бота не доходит. Результат для активного графа сервер проверяет как публикацию: ошибки → HTTP 422 со всеми code@nodeId, граф не изменён.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, replacements: { type: "array", items: { type: "object", properties: { find: { type: "string" }, replace: { type: "string" } }, required: ["find", "replace"] } }, preview: { type: "boolean", description: "true = только отчёт о числе совпадений, без сохранения" }, backup: { type: "boolean", description: "снять авто-бэкап предыдущего состояния перед правкой (по умолчанию true)" } }, required: ["graphId", "replacements"] } },
673
683
  { name: "dry_run", description: "Прогнать сценарий без публикации. kind: command|callback|text. Внешние действия (HTTP, CRM, таблицы, письма, ИИ) пропускаются (skipped: dry-run). Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, kind: { type: "string", enum: ["command", "callback", "text"] }, value: { type: "string" }, fromUsername: { type: "string" }, presetVariables: { type: "object" }, presetTags: { type: "array", items: { type: "string" } } }, required: ["graphId", "kind", "value"] } },
674
- { name: "publish_graph", description: "Опубликовать граф. Вернёт publishedGraphId; при отказе проверок — ошибка HTTP 422 со всеми причинами построчно (code@nodeId: message). Сценарий-вебхук (источник WEBHOOK) этим инструментом не публикуется — HTTP 409, его публикуют в вебе.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
684
+ { name: "publish_graph", description: "Опубликовать граф. У бота ОДИН активный сценарий: публикация черновика заменяет содержимое активного (id живого графа сохраняется); прежняя активная версия, которой нет ни в одном черновике бота (правки вживую), сохраняется черновиком «… (была активной до дата время)». Вернёт publishedGraphId; при отказе проверок — ошибка HTTP 422 со всеми причинами построчно (code@nodeId: message). Сценарий-вебхук (источник WEBHOOK) этим инструментом не публикуется — HTTP 409, его публикуют в вебе.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
675
685
  { name: "import_funnel", description: "Всё за раз: создать граф, залить узлы/рёбра, (опц.) dry-run /start, опубликовать. Граф можно передать инлайном (graph) или файлом (graphFile).", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" }, graph: { type: "object" }, graphFile: { type: "string", description: "Путь к локальному JSON графа вместо инлайн graph; поддерживается ~" }, dryRun: { type: "boolean" }, publish: { type: "boolean" } }, required: ["botId"] } },
676
686
  { name: "list_templates", description: "Список готовых шаблонов воронок (id, имя, описание). Можно стартовать граф из шаблона вместо сборки с нуля.", inputSchema: { type: "object", properties: {} } },
677
687
  { name: "create_graph_from_template", description: "Создать граф (DRAFT) из шаблона (см. list_templates). Возвращает граф с id — дальше правь через update_graph.", inputSchema: { type: "object", properties: { botId: { type: "string" }, templateId: { type: "string" }, name: { type: "string" } }, required: ["botId", "templateId"] } },
@@ -686,8 +696,8 @@ const TOOLS = [
686
696
  { name: "graph_analytics", description: "Аналитика прохождения сценария по узлам (GET /api/bots/graphs/{graphId}/analytics): сколько пользователей дошло до каждого узла — видно, где отваливается воронка. Read-only.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
687
697
  { 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"] } },
688
698
  { name: "list_links", description: "Стартовые (трекинговые) ссылки бота с UTM (GET /api/bots/{botId}/links): code, метки, число стартов. Это точки входа в воронку. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
689
- { name: "site_list", description: "Сайты пользователя (раздел «Страницы», GET /api/bots/pages): id, title, mode (BLOCKS — сайт из блоков, CODE — файлы/Mini App), url (основной адрес), publishedRevision. Read-only.", inputSchema: { type: "object", properties: {} } },
690
- { 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"] } },
699
+ { name: "site_list", description: "Сайты пользователя (раздел «Страницы», GET /api/bots/pages): id, title, mode (BLOCKS — сайт из блоков, CODE — файлы/Mini App), url (основной адрес), publishedRevision, status (ACTIVE | BLOCKED — заблокирован модерацией: посетители видят HTTP 451, удалить такой сайт нельзя — 409) и blockedReason. Read-only.", inputSchema: { type: "object", properties: {} } },
700
+ { 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). Число сайтов ограничено тарифом (free — 1, «Старт» — 3, «Про» — 10, с «Бизнеса» — без лимита): сверх лимита — HTTP 402 с причиной и upgradeUrl (смена тарифа только в браузере, передай ссылку пользователю). Заблокированный модерацией сайт в site_list имеет status=BLOCKED и blockedReason, посетители видят HTTP 451.", inputSchema: { type: "object", properties: { title: { type: "string" }, slug: { type: "string" }, template: { type: "string", enum: ["starter", "blank", "mini-landing"] } }, required: ["title"] } },
691
701
  { 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"] } },
692
702
  { name: "site_schema", description: "JSON Schema модели сайта (model) и операций правки (ops) — какие блоки и поля бывают (GET /api/bots/pages/schema). Читай перед первой правкой.", inputSchema: { type: "object", properties: {} } },
693
703
  { name: "site_edit", description: "Правка сайта операциями — всё или ничего (POST /api/bots/pages/{siteId}/document/ops). " +
@@ -760,14 +770,14 @@ const TOOLS = [
760
770
  { name: "dialog_messages", description: "Переписка с подписчиком, свежие сверху (GET /api/bots/{botId}/users/{chatId}/messages, page/size). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["botId", "chatId"] } },
761
771
  { name: "dialog_reply", description: "ОТПРАВИТЬ сообщение подписчику от имени оператора (POST /api/bots/{botId}/users/{chatId}/messages) — как ответ из раздела «Диалоги». Уходит РЕАЛЬНОМУ человеку: только по явной просьбе пользователя. Бот при этом не останавливается.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, text: { type: "string" } }, required: ["botId", "chatId", "text"] } },
762
772
  // ---- Паритет платформы: журнал вызовов, оператор, заявки, виджет, подписчики, бот ----
763
- { name: "integration_calls", description: "Журнал вызовов внешних сервисов из сценариев (GET /api/bots/integrations/calls): время, подключение, действие, ok/ошибка, код, попытки, correlationId (= runId прогона). Фильтры: connectionId (из list_integrations), ok (true — только успешные, false — только упавшие), limit (1–200). Так проверяют, что CRM/таблица/HTTP реально получили данные. Read-only. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { connectionId: { type: "string" }, ok: { type: "boolean" }, limit: { type: "number" } } } },
764
- { name: "dialog_handoff", description: "Передача диалога оператору (POST /api/bots/{botId}/users/{chatId}/handoff {active}): active:true — бот и ИИ молчат, входящие копятся в «Диалогах», отвечает человек (dialog_reply); active:false — «Вернуть боту». Текущее состояние — поле handoff в bot_user_get. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, active: { type: "boolean" } }, required: ["botId", "chatId", "active"] } },
765
- { name: "site_lead_status", description: "Статус заявки сайта (PATCH /api/bots/pages/{siteId}/leads/{leadId} {status}): NEW → IN_PROGRESS → DONE | REJECTED. leadId — из site_leads. Недопустимый переход → ошибка 409. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, status: { type: "string", enum: ["NEW", "IN_PROGRESS", "DONE", "REJECTED"] } }, required: ["siteId", "leadId", "status"] } },
773
+ { name: "integration_calls", description: "Журнал вызовов внешних сервисов из сценариев (GET /api/bots/integrations/calls): время, подключение, действие, ok/ошибка, код, попытки, correlationId (= runId прогона). Фильтры: connectionId (из list_integrations), ok (true — только успешные, false — только упавшие), limit (1–200). Так проверяют, что CRM/таблица/HTTP реально получили данные. Read-only.", inputSchema: { type: "object", properties: { connectionId: { type: "string" }, ok: { type: "boolean" }, limit: { type: "number" } } } },
774
+ { name: "dialog_handoff", description: "Передача диалога оператору (POST /api/bots/{botId}/users/{chatId}/handoff {active}): active:true — бот и ИИ молчат, входящие копятся в «Диалогах», отвечает человек (dialog_reply); active:false — «Вернуть боту». Текущее состояние — поле handoff в bot_user_get.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, active: { type: "boolean" } }, required: ["botId", "chatId", "active"] } },
775
+ { name: "site_lead_status", description: "Статус заявки сайта (PATCH /api/bots/pages/{siteId}/leads/{leadId} {status}): NEW → IN_PROGRESS → DONE | REJECTED. leadId — из site_leads. Недопустимый переход → ошибка 409.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, status: { type: "string", enum: ["NEW", "IN_PROGRESS", "DONE", "REJECTED"] } }, required: ["siteId", "leadId", "status"] } },
766
776
  { name: "integration_update", description: "Изменить подключение (PUT /api/bots/integrations/{connectionId}): title — новое название; creds — новые ключи доступа (заменяют старые; поля — как в connect_integration для этого провайдера). Передай хотя бы одно.", inputSchema: { type: "object", properties: { connectionId: { type: "string" }, title: { type: "string" }, creds: { type: "object", additionalProperties: { type: "string" } } }, required: ["connectionId"] } },
767
777
  { name: "web_widget_settings", description: "Вид чат-виджета сайта (бот platform WEB). Без settings — прочитать (GET /api/bots/web/{botId}/settings: settings, brandingRemovable, snippet). С settings — изменить (PUT): переданные поля накладываются на текущие, остальные сохраняются; сервер проверяет значения (уходят в разметку чужих сайтов).", inputSchema: { type: "object", properties: { botId: { type: "string" }, settings: { type: "object", description: "Поля настроек виджета для изменения (имена — как в ответе чтения)" } }, required: ["botId"] } },
768
778
  { name: "bot_users_import", description: "Добавить подписчикам метки и поля (POST /api/bots/{botId}/users/import): rows — [{chatId, username?, tags?: [строки], variables?: {ключ: значение}}], до 10 000 строк. Метки ДОБАВЛЯЮТСЯ; переменная пишется, только если у подписчика её ещё нет (существующие не перезаписываются); новый chatId создаёт подписчика (в пределах лимита тарифа). Снять метку или перезаписать поле через API нельзя.", inputSchema: { type: "object", properties: { botId: { type: "string" }, rows: { type: "array", items: { type: "object", properties: { chatId: { type: "string" }, username: { type: "string" }, tags: { type: "array", items: { type: "string" } }, variables: { type: "object" } }, required: ["chatId"] } } }, required: ["botId", "rows"] } },
769
779
  { name: "bot_runs", description: "Журнал прогонов сценариев бота. Без runId — список свежих прогонов (GET /api/bots/{botId}/runs, page/size); с runId — один прогон с шагами (GET /api/bots/runs/{runId}, в т.ч. headless-прогоны вебхук-сценариев и расписаний). По подписчику — bot_user_runs, по сценарию — scenario_runs. Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, runId: { type: "string" }, page: { type: "number" }, size: { type: "number" } } } },
770
- { name: "scenario_runs", description: "Журнал прогонов одного сценария (GET /api/bots/graphs/{graphId}/runs, page/size), включая headless-прогоны без чата — вебхук-сценарии, заявки сайта, TRIGGER_SCHEDULE. Доступ — как к просмотру графа. Шаги прогона — bot_runs {runId}. Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»). Read-only. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["graphId"] } },
780
+ { name: "scenario_runs", description: "Журнал прогонов одного сценария (GET /api/bots/graphs/{graphId}/runs, page/size), включая headless-прогоны без чата — вебхук-сценарии, заявки сайта, TRIGGER_SCHEDULE. Доступ — как к просмотру графа. Шаги прогона — bot_runs {runId}. Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»). Read-only.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["graphId"] } },
771
781
  { name: "bot_delete", description: "УДАЛИТЬ бота навсегда (DELETE /api/bots/{botId}) вместе с каналами, сценариями, подписчиками, журналами, рассылками и ссылками. Только владелец. Необратимо: вызывай только по явной просьбе пользователя и с confirm:true. Временно выключить — bot_stop.", inputSchema: { type: "object", properties: { botId: { type: "string" }, confirm: { type: "boolean", description: "true — пользователь явно подтвердил удаление" } }, required: ["botId", "confirm"] } },
772
782
  // ---- Запись на слоты (бронирование) ----
773
783
  { name: "booking_calendar_list", description: "Календари записи пользователя (GET /api/bots/booking/calendars): id, name, zone, slotMinutes, hours, exceptions, botId. id — calendarId для действий сценария booking_slots/booking_book/booking_cancel. Read-only.", inputSchema: { type: "object", properties: {} } },
@@ -780,6 +790,24 @@ const TOOLS = [
780
790
  { name: "booking_create", description: "Записать вручную на слот (POST /api/bots/booking/calendars/{calendarId}/bookings {slotAt, name, phone}): slotAt — at из booking_slots (ISO-8601 с зоной). Слот занят → понятная ошибка «занят», слота нет в сетке → ошибка 400.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, slotAt: { type: "string" }, name: { type: "string" }, phone: { type: "string" } }, required: ["calendarId", "slotAt"] } },
781
791
  { name: "booking_cancel", description: "Отменить бронь и освободить слот (POST /api/bots/booking/calendars/{calendarId}/bookings/{bookingId}/cancel). Повторная отмена — без ошибки. bookingId — из booking_list.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
782
792
  { name: "site_templates", description: "Библиотека шаблонов блоков сайта (GET /api/bots/pages/templates): {categories: [{id, title, description?}], templates: [{id, category, title, description?, blocks: сколько блоков вставится}]}. Вставка — site_edit add_template {container, templateId, after?} (results.id — первый блок, results.ids — все); дальше блоки правятся как обычные. category — фильтр по id категории.", inputSchema: { type: "object", properties: { category: { type: "string" } } } },
793
+ // ---- Паритет платформы, часть 2: бот, база, агент, заявки, подписчики, каналы, ссылки, аналитика ----
794
+ { name: "bot_rename", description: "Переименовать бота (PATCH /api/bots/{botId} {name}): отображаемое имя в кабинете, до 250 символов. Пустое name сбрасывает имя на @username. Имя в самом Telegram/MAX не меняется.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
795
+ { name: "bot_change_token", description: "Сменить токен бота (POST /api/bots/{botId}/token {token}): сервер проверяет токен и перезапускает приём сообщений; сценарии, подписчики и ссылки остаются. Нужен, если токен перевыпущен в @BotFather (/revoke) или MAX. Неверный или чужой токен → ошибка с причиной. Владелец или ADMIN.", inputSchema: { type: "object", properties: { botId: { type: "string" }, token: { type: "string", description: "Новый токен бота" } }, required: ["botId", "token"] } },
796
+ { name: "bot_channel_list", description: "Дополнительные Telegram-боты (мультиканальность) этого бота (GET /api/bots/{botId}/channels): id, username, name, active. Это НЕ каналы/группы для постинга — те в list_channels. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
797
+ { name: "bot_channel_add", description: "Добавить к боту ещё одного Telegram-бота как канал (POST /api/bots/{botId}/channels {token, name?}): он отвечает теми же сценариями. Токен проверяется в Telegram; токен, уже подключённый где-то ещё, не принимается.", inputSchema: { type: "object", properties: { botId: { type: "string" }, token: { type: "string" }, name: { type: "string" } }, required: ["botId", "token"] } },
798
+ { name: "bot_channel_delete", description: "Отключить дополнительного Telegram-бота от бота (DELETE /api/bots/{botId}/channels/{channelId}). channelId — из bot_channel_list. Только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { botId: { type: "string" }, channelId: { type: "string" }, confirm: { type: "boolean" } }, required: ["botId", "channelId", "confirm"] } },
799
+ { name: "link_create", description: "Создать стартовую ссылку бота (POST /api/bots/{botId}/links {name, targetNodeId?}): code для t.me/<бот>?start=<code>, счётчик стартов. targetNodeId — узел сценария, на который ведёт диплинк; для одного узла повторный вызов вернёт существующую ссылку.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" }, targetNodeId: { type: "string" } }, required: ["botId"] } },
800
+ { name: "link_delete", description: "Удалить стартовую ссылку (DELETE /api/bots/links/{linkId}); id — из list_links. Разосланные ссылки перестанут считаться. Только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { linkId: { type: "string" }, confirm: { type: "boolean" } }, required: ["linkId", "confirm"] } },
801
+ { name: "utm_sources", description: "UTM-источники подписчиков бота (GET /api/bots/{botId}/utm-sources): по каким меткам utm_* люди входили в сценарии и сколько их. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
802
+ { name: "ab_results", description: "Результаты A/B-теста (GET /api/bots/graphs/{graphId}/ab-results?branchNodeId&period): статистика по вариантам узла-развилки. branchNodeId — id узла A/B в графе; period — «24h», «7d», «30d» (по умолчанию 7d). Read-only.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, branchNodeId: { type: "string" }, period: { type: "string" } }, required: ["graphId", "branchNodeId"] } },
803
+ { name: "kb_delete", description: "УДАЛИТЬ базу знаний целиком со всеми документами (DELETE /api/bots/kb/{kbId}). База ИИ-агента так не удаляется (ошибка KB_OWNED_BY_AGENT) — удаляй агента. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { kbId: { type: "string" }, confirm: { type: "boolean" } }, required: ["kbId", "confirm"] } },
804
+ { name: "agent_unpublish", description: "Снять ИИ-агента с публикации (POST /api/bots/agents/{agentId}/unpublish): агент возвращается в черновик и перестаёт отвечать. В ответе — agent и сценарии, которые сейчас на нём работают (их стоит проверить).", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
805
+ { name: "agent_delete", description: "УДАЛИТЬ ИИ-агента (DELETE /api/bots/agents/{agentId}). Пока агент или его база подключены к сценариям — ошибка AGENT_IN_USE со списком сценариев: сначала отключи его там. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, confirm: { type: "boolean" } }, required: ["agentId", "confirm"] } },
806
+ { name: "site_leads_mark_read", description: "Отметить все заявки сайта прочитанными (POST /api/bots/pages/{siteId}/leads/read): обнуляет счётчик unread из site_leads. Статусы обработки не меняются (это site_lead_status).", inputSchema: { type: "object", properties: { siteId: { type: "string" } }, required: ["siteId"] } },
807
+ { name: "site_lead_delete", description: "УДАЛИТЬ заявку сайта (DELETE /api/bots/pages/{siteId}/leads/{leadId}); leadId — из site_leads. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, confirm: { type: "boolean" } }, required: ["siteId", "leadId", "confirm"] } },
808
+ { name: "site_leads_export", description: "Все заявки сайта в CSV (GET /api/bots/pages/{siteId}/leads.csv). savePath — сохранить в локальный файл и вернуть путь (для больших выгрузок); без него CSV возвращается текстом. Read-only.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, savePath: { type: "string" } }, required: ["siteId"] } },
809
+ { name: "bot_users_export", description: "Выгрузка всех подписчиков бота (GET /api/bots/{botId}/users/export?format=csv|json): chatId, имя, метки, переменные. savePath — сохранить в локальный файл и вернуть путь; без него данные возвращаются текстом. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, format: { type: "string", enum: ["csv", "json"] }, savePath: { type: "string" } }, required: ["botId"] } },
810
+ { name: "bot_user_reset", description: "Сбросить сессию подписчика (DELETE /api/bots/{botId}/sessions/{chatId}): стираются его позиция в сценарии, метки и переменные, и он пропадает из list_bot_users; при следующем сообщении начнёт как новый. Для повторного прохождения воронки при тесте. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, confirm: { type: "boolean" } }, required: ["botId", "chatId", "confirm"] } },
783
811
  ];
784
812
 
785
813
  async function handleCall(params) {
@@ -1254,6 +1282,114 @@ async function handleCall(params) {
1254
1282
  await api(`/api/bots/${botId}`, { method: "DELETE" });
1255
1283
  return okResult(`🗑️ Бот ${botId} удалён вместе со сценариями и подписчиками.`);
1256
1284
  }
1285
+ // ---- Паритет платформы, часть 2 ----
1286
+ case "bot_rename": {
1287
+ if (typeof a.name !== "string") throw new Error("name — строка (пустая сбрасывает имя на @username).");
1288
+ return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}`, { method: "PATCH", body: { name: a.name } }));
1289
+ }
1290
+ case "bot_change_token": {
1291
+ const botId = uuidArg(a.botId, "botId");
1292
+ const token = String(a.token ?? "").trim();
1293
+ if (!token) throw new Error("Передай token — новый токен бота.");
1294
+ try {
1295
+ return okResult(await api(`/api/bots/${botId}/token`, { method: "POST", body: { token } }));
1296
+ } catch (e) {
1297
+ // 400 — строка-причина (неверный токен, уже подключён к другому боту); сам токен в ошибку не кладём.
1298
+ if (e.status === 400) throw new Error(`Токен не принят: ${typeof e.data === "string" && e.data ? e.data : "проверь токен"}.`);
1299
+ throw e;
1300
+ }
1301
+ }
1302
+ case "bot_channel_list": return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/channels`));
1303
+ case "bot_channel_add": {
1304
+ const botId = uuidArg(a.botId, "botId");
1305
+ const token = String(a.token ?? "").trim();
1306
+ if (!token) throw new Error("Передай token — токен дополнительного Telegram-бота.");
1307
+ try {
1308
+ return okResult(await api(`/api/bots/${botId}/channels`, { method: "POST", body: { token, name: a.name || undefined } }));
1309
+ } catch (e) {
1310
+ if (e.status === 400) throw new Error(`Канал не добавлен: ${typeof e.data === "string" && e.data ? e.data : "токен не принят"}.`);
1311
+ throw e;
1312
+ }
1313
+ }
1314
+ case "bot_channel_delete": {
1315
+ const botId = uuidArg(a.botId, "botId");
1316
+ const channelId = uuidArg(a.channelId, "channelId");
1317
+ if (a.confirm !== true) throw new Error("Отключение канала необратимо. Спроси пользователя и повтори с confirm:true.");
1318
+ await api(`/api/bots/${botId}/channels/${channelId}`, { method: "DELETE" });
1319
+ return okResult(`🗑️ Канал ${channelId} отключён от бота ${botId}.`);
1320
+ }
1321
+ case "link_create": {
1322
+ const body = { name: a.name || undefined };
1323
+ if (a.targetNodeId) body.targetNodeId = uuidArg(a.targetNodeId, "targetNodeId");
1324
+ return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/links`, { method: "POST", body }));
1325
+ }
1326
+ case "link_delete": {
1327
+ const linkId = uuidArg(a.linkId, "linkId");
1328
+ if (a.confirm !== true) throw new Error("Удаление ссылки необратимо. Спроси пользователя и повтори с confirm:true.");
1329
+ await api(`/api/bots/links/${linkId}`, { method: "DELETE" });
1330
+ return okResult(`🗑️ Ссылка ${linkId} удалена.`);
1331
+ }
1332
+ case "utm_sources": return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/utm-sources`));
1333
+ case "ab_results": {
1334
+ const q = { branchNodeId: uuidArg(a.branchNodeId, "branchNodeId") };
1335
+ if (a.period != null && a.period !== "") {
1336
+ const p = String(a.period).trim();
1337
+ if (!/^\d{1,4}[hd]$/.test(p)) throw new Error(`period — число с h или d (24h, 7d, 30d), получено: ${p}.`);
1338
+ q.period = p;
1339
+ }
1340
+ return okResult(await api(`/api/bots/graphs/${uuidArg(a.graphId, "graphId")}/ab-results${qs(q)}`));
1341
+ }
1342
+ case "kb_delete": {
1343
+ const kbId = idArg(a.kbId, "kbId");
1344
+ if (a.confirm !== true) throw new Error("Удаление базы знаний необратимо. Спроси пользователя и повтори с confirm:true.");
1345
+ try {
1346
+ await api(`/api/bots/kb/${kbId}`, { method: "DELETE" });
1347
+ } catch (e) {
1348
+ if (e.status === 409) throw new Error("Это база ИИ-агента (KB_OWNED_BY_AGENT): отдельно не удаляется — удали агента (agent_delete).");
1349
+ throw e;
1350
+ }
1351
+ return okResult(`🗑️ База знаний ${kbId} удалена со всеми документами.`);
1352
+ }
1353
+ case "agent_unpublish": return okResult(await api(`/api/bots/agents/${idArg(a.agentId, "agentId")}/unpublish`, { method: "POST" }));
1354
+ case "agent_delete": {
1355
+ const agentId = idArg(a.agentId, "agentId");
1356
+ if (a.confirm !== true) throw new Error("Удаление агента необратимо. Спроси пользователя и повтори с confirm:true.");
1357
+ try {
1358
+ await api(`/api/bots/agents/${agentId}`, { method: "DELETE" });
1359
+ } catch (e) {
1360
+ if (e.status === 409) {
1361
+ const refs = Array.isArray(e.data?.scenarios) ? e.data.scenarios.map((s) => s?.graphName || s?.graphId).filter(Boolean) : [];
1362
+ throw new Error(`Агент подключён к сценариям (AGENT_IN_USE)${refs.length ? `: ${refs.join(", ")}` : ""}. Сначала отключи его в них.`);
1363
+ }
1364
+ throw e;
1365
+ }
1366
+ return okResult(`🗑️ Агент ${agentId} удалён.`);
1367
+ }
1368
+ case "site_leads_mark_read": {
1369
+ const siteId = uuidArg(a.siteId, "siteId");
1370
+ await api(`/api/bots/pages/${siteId}/leads/read`, { method: "POST" });
1371
+ return okResult(`✅ Все заявки сайта ${siteId} отмечены прочитанными.`);
1372
+ }
1373
+ case "site_lead_delete": {
1374
+ const siteId = uuidArg(a.siteId, "siteId");
1375
+ const leadId = uuidArg(a.leadId, "leadId");
1376
+ if (a.confirm !== true) throw new Error("Удаление заявки необратимо. Спроси пользователя и повтори с confirm:true.");
1377
+ await api(`/api/bots/pages/${siteId}/leads/${leadId}`, { method: "DELETE" });
1378
+ return okResult(`🗑️ Заявка ${leadId} удалена.`);
1379
+ }
1380
+ case "site_leads_export": return exportResult(await api(`/api/bots/pages/${uuidArg(a.siteId, "siteId")}/leads.csv`), a.savePath);
1381
+ case "bot_users_export": {
1382
+ const format = String(a.format || "csv").trim().toLowerCase();
1383
+ if (format !== "csv" && format !== "json") throw new Error(`format: csv | json, получено: ${format}.`);
1384
+ return exportResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/users/export?format=${format}`), a.savePath);
1385
+ }
1386
+ case "bot_user_reset": {
1387
+ const botId = uuidArg(a.botId, "botId");
1388
+ const chatId = chatIdArg(a.chatId);
1389
+ if (a.confirm !== true) throw new Error("Сброс сессии стирает метки и переменные подписчика. Спроси пользователя и повтори с confirm:true.");
1390
+ await api(`/api/bots/${botId}/sessions/${chatId}`, { method: "DELETE" });
1391
+ return okResult(`♻️ Сессия подписчика ${chatId} сброшена: при следующем сообщении он начнёт как новый.`);
1392
+ }
1257
1393
  // ---- Боты ----
1258
1394
  case "create_bot": {
1259
1395
  const platform = String(a.platform || "").trim().toUpperCase();