@retensy/mcp 0.25.1 → 0.27.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.
- package/README.md +43 -5
- package/package.json +2 -2
- package/skills/build-bot-funnel/SKILL.md +2 -1
- package/src/index.mjs +52 -8
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
-
- 🤖 **Сценарии ботов**: `list_bots`, `list_graphs`, `list_channels`, `get_graph`, `create_graph`, `update_graph`, `edit_graph_live`, `patch_graph`, `dry_run`, `publish_graph`, `import_funnel`, `list_templates`, `create_graph_from_template`, `clone_graph`, `copy_graph`, `rename_graph`, `set_active_graph`, `delete_graph`, `upload_file`, `list_files`, `delete_file`, `graph_analytics`, `list_bot_users`, `list_links` (+ `setup`/`set_token`).
|
|
51
|
+
- 🤖 **Сценарии ботов**: `list_bots`, `list_graphs`, `list_channels`, `get_graph`, `create_graph`, `update_graph`, `edit_graph_live`, `patch_graph`, `dry_run`, `publish_graph`, `unpublish_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`).
|
|
52
52
|
- 🔌 **Боты и сервисы**: `create_bot` (Telegram/MAX по токену), `bot_stop`/`bot_resume`, `connect_integration`/`disconnect_integration` (amoCRM, Битрикс24, GetCourse, Я.Метрика, ЮKassa; Google Таблицы — ссылкой на вход Google; креды хранятся зашифрованными и не возвращаются), `integration_catalog`/`integration_status`/`integration_test` (каталог Integration Core и живая проверка подключения), `integration_ingress_url` (URL ингресса «Внешних событий» — Tilda, JivoSite, WooCommerce, amoCRM, Битрикс24, GetCourse, CloudPayments, Robokassa, Т-Банк, Prodamus — для `TRIGGER_WEBHOOK`), `channel_post` (пост в канал Telegram/MAX).
|
|
53
53
|
- 📣 **Рассылки**: `broadcast_preview` (размер аудитории), `broadcast_send` (сейчас или по расписанию, сразу по нескольким ботам; сообщения или запуск сценария; полный фильтр аудитории `audienceFilter`, дедуп `dedup` и распределённый запуск `stagger` по ботам), `broadcast_list`/`broadcast_get`/`broadcast_cancel`, повторяющиеся (`broadcast_recurring`), черновики (`broadcast_drafts`, `broadcast_duplicate`), мульти-бот группы (`broadcast_group` — preview/list/get/pause/resume/cancel) + скилл `send-broadcast`.
|
|
54
54
|
- 👥 **Аудитория подписчиков**: `audience_search`/`audience_fields` — кросс-бот поиск и фильтр (теги, даты, статус, условия по переменным), `audience_tag_edit`/`audience_variable_edit` — массовое редактирование тегов/переменных, `audience_export` — потоковая выгрузка выборки в CSV/XLSX, `dialog_search` — поиск по «Диалогам» (включая текст сообщений).
|
|
@@ -111,10 +111,10 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
111
111
|
|
|
112
112
|
## Авторизация — персональный токен
|
|
113
113
|
|
|
114
|
-
Токен
|
|
114
|
+
Токен действует от твоего имени **в пределах выбранных прав** (см. «Права токена» ниже).
|
|
115
115
|
|
|
116
116
|
1. Залогинься на https://bots.retensy.com → открой **`/bots/mcp-tokens`**.
|
|
117
|
-
2.
|
|
117
|
+
2. Выбери права токена (пресет «Только чтение», «Полный доступ» или свой набор) и, при желании, срок действия → создай токен → скопируй секрет `zmcp_...` (показывается один раз).
|
|
118
118
|
3. Передай токен любым способом:
|
|
119
119
|
- **просто пришли его агенту в чат** — он вызовет инструмент `set_token` и сохранит токен в `~/.retensy-bot-graph/token` (применяется сразу, без рестарта), **или**
|
|
120
120
|
- `env` в `.mcp.json` (Вариант B), **или**
|
|
@@ -126,6 +126,43 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
126
126
|
> и управление подключениями сервисов (`/api/integrations` — `connect_integration`/`disconnect_integration`/
|
|
127
127
|
> `integration_update` и остальные `integration_*`), не только `/api/bots`. Отдельного токена под эти инструменты не нужно.
|
|
128
128
|
|
|
129
|
+
### Права токена (scopes)
|
|
130
|
+
|
|
131
|
+
С 17.10.2026 у токена есть права. Их проверяет **сервер на каждом запросе**, а не MCP-клиент, поэтому прямой
|
|
132
|
+
вызов API (curl и т.п.) их тоже не обходит; MCP-клиент только объясняет отказ. Но права **крупные** — выдавай
|
|
133
|
+
токену только то, что нужно для задачи.
|
|
134
|
+
|
|
135
|
+
| Право | Что разрешает | Примеры инструментов |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `read` | любое чтение: списки, карточки, аналитика, превью, поиск, `dry_run`. **Включает персональные данные**: подписчики, переписка в диалогах, заявки с сайтов и их файлы | `list_bots`, `get_graph`, `site_leads`, `dialog_messages`, `broadcast_preview` |
|
|
138
|
+
| `write:bots` | создание и изменение ботов, сценариев, цепочек, интеграций, ИИ-агентов, базы знаний, черновиков рассылок. Правка опубликованного сценария сразу меняет ответы бота подписчикам; можно сменить ключи и вебхуки интеграций; `agent_test_chat` тратит ИИ-бюджет, `integration_test` ходит во внешний сервис | `create_graph`, `publish_graph`, `chain_update`, `connect_integration`, `agent_test_chat` |
|
|
139
|
+
| `write:sites` | сайты, лиды, VK-лендинги, статьи | `site_create`, `site_edit`, `site_publish`, `vk_landing_update`, `article_publish` |
|
|
140
|
+
| `crm` | теги/переменные подписчиков, импорт, передача диалога оператору, записи. `booking_confirm`/`booking_reject` отправляют клиенту сообщение | `audience_tag_edit`, `bot_users_import`, `dialog_handoff`, `booking_create` |
|
|
141
|
+
| `broadcast` | всё, что доходит до живых людей без участия человека: рассылки, ответы, посты, а также **включённая цепочка без согласования постов** (approvalMode `NONE`/`AI_ONLY`) и **публикация/правка/активация сценария с `TRIGGER_SCHEDULE`** | `broadcast_send`, `dialog_reply`, `channel_post`, `vk_post`, `chain_approve`, `chain_enable` (без согласования) |
|
|
142
|
+
| `billing` | деньги и ключи, которые тратят бюджет | `ai_pack_purchase`, `agent_api_key_create` |
|
|
143
|
+
| `destructive` | любое удаление и массовая выгрузка ПДн | `delete_graph`, `bot_delete`, `audience_export`, `site_leads_export` |
|
|
144
|
+
|
|
145
|
+
- **Пресеты:** «Только чтение» = `read`; «Полный доступ» = все семь. По умолчанию в кабинете предлагается
|
|
146
|
+
`read` + `write:bots` + `write:sites` — собирать ботов и сайты. Это **не «безопасный»** набор: он видит ПДн
|
|
147
|
+
подписчиков и заявок и меняет живые ответы ботов и ключи интеграций. В нём нет только рассылок, оплаты,
|
|
148
|
+
удаления и выгрузки.
|
|
149
|
+
- **Цепочки и расписание** с одним `write:bots`: цепочку можно создать выключенной или с согласованием каждого
|
|
150
|
+
поста (`approvalMode: ALL`), сценарий по расписанию, с отправкой в заданный чат/канал (`target.mode` chat/expression)
|
|
151
|
+
или вебхук-сценарий с получателем из тела запроса (`recipientFrom`) — оставить черновиком. Включить/опубликовать — нужен `broadcast`.
|
|
152
|
+
То же для сценария с письмом на любой адрес, кроме ровно `{{var.email}}` подписчика (и если сценарий сам не
|
|
153
|
+
перезаписывает `email`), и для сценария с триггером события Telegram/комментария и шагами отправки — там
|
|
154
|
+
«ответ в текущий чат» уходит в канал или группу. Правка живого вебхук-сценария и новый секрет живого
|
|
155
|
+
вебхука, который шлёт людям, тоже требуют `broadcast`.
|
|
156
|
+
- **Нет права** → сервер отвечает `403 {"error":"insufficient_scope","requiredScope":"…"}`, а инструмент
|
|
157
|
+
возвращает ошибку «Токену не хватает права «X» …». Это не сбой: добавь право токену кнопкой
|
|
158
|
+
«Изменить права» на `/bots/mcp-tokens` (действует со следующего запроса, переподключать клиента не нужно)
|
|
159
|
+
или выпусти новый токен.
|
|
160
|
+
- **Старые токены** (выпущены до появления прав) работают как раньше — с полным доступом; в кабинете они
|
|
161
|
+
помечены «полный доступ (старый токен)». Рекомендуем ограничить им права.
|
|
162
|
+
- **Срок действия** (30/90/365 дней или бессрочно) задаётся при создании; истёкший токен отвечает 401 и не
|
|
163
|
+
ротируется (400 `token_expired`) — выпусти новый. Ротация сохраняет права и длительность срока; API ротации
|
|
164
|
+
принимает и новые права (`scopes`/`preset`) — так старый токен переводят на явные права.
|
|
165
|
+
|
|
129
166
|
> **Не знаешь, что делать?** Скажи агенту «настрой подключение» — он вызовет `setup`, объяснит шаги и попросит токен. Любой инструмент при отсутствии токена тоже вернёт пошаговую инструкцию.
|
|
130
167
|
|
|
131
168
|
> Дев-окружение: `RETENSY_BASE_URL=http://localhost:8066`.
|
|
@@ -167,7 +204,8 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
167
204
|
| `edit_graph_live(graphId, graphFile\|graph\|nodes,edges)` | правка живого графа НА МЕСТЕ + авто-бэкап (рекомендуется для прода) |
|
|
168
205
|
| `patch_graph(graphId, replacements)` | строковые замены в JSON графа на сервере (для больших/живых графов) |
|
|
169
206
|
| `dry_run(graphId, kind, value)` | прогон без публикации |
|
|
170
|
-
| `publish_graph(graphId)` | публикация (вернёт `errors[]` при провале) |
|
|
207
|
+
| `publish_graph(graphId, secondary?)` | публикация (вернёт `errors[]` при провале); `secondary: true` — дополнительный сценарий рядом с основным (запускают VK-лендинги и ссылки с `targetGraphId`) |
|
|
208
|
+
| `unpublish_graph(graphId)` | снять дополнительный сценарий с публикации (основной — 409) |
|
|
171
209
|
| `import_funnel(botId, name, graphFile\|graph)` | всё за раз: create → update → dry-run → publish |
|
|
172
210
|
| `list_templates()` | готовые шаблоны воронок |
|
|
173
211
|
| `create_graph_from_template(botId, templateId, name)` | граф из шаблона (DRAFT) |
|
|
@@ -433,7 +471,7 @@ RETENSY_MCP_TOKEN=zmcp_... node src/index.mjs # стартует stdio MCP-с
|
|
|
433
471
|
|
|
434
472
|
## Безопасность
|
|
435
473
|
|
|
436
|
-
Токен = доступ к аккаунту по API. Не коммить
|
|
474
|
+
Токен = доступ к аккаунту по API в пределах его прав. Выдавай агенту минимально нужные права (для знакомства — «Только чтение»). Не коммить токен; держи в `env`. В конфигах храни ссылку `${RETENSY_MCP_TOKEN}`, не само значение.
|
|
437
475
|
|
|
438
476
|
## Лицензия
|
|
439
477
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "MCP server to build and publish Telegram and MAX bot funnels/automations, broadcasts, service integrations, block-based websites (incl. free-layout Zero blocks, designs, templates, custom domains, form leads) and Markdown blog articles in the Retensy Bots service. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node src/index.mjs",
|
|
23
23
|
"check": "node --check src/index.mjs",
|
|
24
|
-
"test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs",
|
|
24
|
+
"test": "node scripts/smoke.mjs && node scripts/telemetry.test.mjs && node scripts/live-edit-422.test.mjs && node scripts/broadcast-connect.test.mjs && node scripts/integration-core.test.mjs && node scripts/site-init.test.mjs && node scripts/ai-agents.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs && node scripts/token-scopes.test.mjs && node scripts/secondary-graphs.test.mjs",
|
|
25
25
|
"validate": "node skills/build-bot-funnel/validate.mjs"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
@@ -67,6 +67,7 @@ description: Собрать воронку (сценарий) бота для с
|
|
|
67
67
|
5b. **Правка СУЩЕСТВУЮЩЕГО / живого сценария — по умолчанию `edit_graph_live`, а НЕ clone+publish.**
|
|
68
68
|
- Когда пользователь просит «поправь сценарий X» (особенно если он уже открыт в редакторе или опубликован) — правь **ТОТ ЖЕ `graphId`** через **`edit_graph_live(graphId, nodes, edges)`**. Он сам снимает авто-бэкап предыдущего состояния (один rolling-граф «🔙 Авто-бэкап») и делает PUT на месте — **id не меняется**. Живой сценарий бота (бот-сценария) — граф с `isActive: true` в `list_graphs` (статус `PUBLISHED`; после `publish_graph` черновика это `publishedGraphId`, а **не** id черновика) — правь его id, иначе правка ляжет в черновик и до бота не дойдёт. Вебхук-сценарий отдельного опубликованного графа не имеет — правь его по его же id.
|
|
69
69
|
- Почему так: бэкенд при PUT/публикации шлёт `external_update` в WS-комнату → открытые редакторы перечитывают граф **вживую** (юзеру не надо перезаходить). Бот читает активный граф **заново из БД на каждое сообщение** → правка живого PUBLISHED-графа применяется **сразу, без отдельной публикации**.
|
|
70
|
+
- **Дополнительный сценарий** (отдельный живой граф под VK-лендинг/рекламную ссылку, основной не трогается): `publish_graph(graphId, secondary: true)` публикует граф НА МЕСТЕ (`publishedGraphId` = тот же id, снимка нет, `active` false, `activeGraphId` бота не меняется); проверки те же (422), нужен основной сценарий у бота (`SECONDARY_NO_MAIN`), основной сам дополнительным не делается (`SECONDARY_IS_MAIN`). Его запускают только `vk_landing_create/update` с `targetGraphId` (ответ показывает `targetGraphStatus`: MAIN/SECONDARY/NOT_PUBLISHED/MISSING) и `link_create` с `targetGraphId` (+`targetNodeId` узла в нём). Подписчик стартует с его /start (или первого триггера/блока); ответы на вопросы, кнопки, задержки продолжаются в нём, команды/ключевые слова — по основному, любой старт (/start, /start <код> не на дополнительный, VK «Начать») ведёт в основной (снимая ожидание вопроса). Нельзя в дополнительном: TRIGGER_SCHEDULE, TRIGGER_SITE_FORM, TRIGGER_TG_EVENT, TRIGGER_COMMENT, TRIGGER_WEBHOOK, TRIGGER_IG_* → 422 `SECONDARY_TRIGGER_UNSUPPORTED` (и при правке вживую); в режиме decoupled → `SECONDARY_UNSUPPORTED`. TRIGGER_PAYMENT в дополнительном — только для подписчика, который сейчас в нём. Клон основного при первой публикации дополнительным получает новые id совпавших узлов (рёбра/конфиги/ссылки на шаги переносятся) — после publish перечитай граф (`get_graph`), старые nodeId недействительны: PUT/`edit_graph_live` живого дополнительного со старыми id узлов основного → HTTP 409 `GRAPH_STALE`. Ссылки «Перейти в сценарий» (graphId другого графа) и текст при перенумерации не меняются. Подписчик возвращается в основной на блоке END дополнительного или по команде/ключевому слову основного; «Оплата прошла» в дополнительном отменяет основной для этой оплаты, только если его триггер реально сработал. Цель-черновик/чужая/основной — подписчик идёт в основной, как раньше. Правки дополнительного — тем же id через `edit_graph_live` (вживую, как основной). Снять — `unpublish_graph(graphId)`; «сделать основным» — `set_active_graph`. В `list_graphs` у него `secondary: true`. Публикация основного (`publish_graph` без `secondary`) дополнительные не удаляет. Дополнительный занимает место в лимите сценариев тарифа.
|
|
70
71
|
- `clone_graph`+`publish_graph` НЕ создают новый живой граф и НЕ песочница: публикация копирует клон (как любой черновик) в id ТЕКУЩЕГО опубликованного графа бота и затирает его содержимое без бэкапа — вместе с живыми правками, сделанными после прошлой публикации; `set_active_graph` на прежний id после этого вернёт уже новое содержимое; открытые редакторы живого графа перечитают его (у кого есть несохранённые правки — получат просьбу обновить страницу). Клон годится, чтобы спокойно собрать крупный рискованный рефактор в черновике, но перед его публикацией сохрани живой граф: `get_graph(graphId, saveToFile)` — откат = залить файл обратно через `edit_graph_live`/`update_graph` с `graphFile`.
|
|
71
72
|
- ⚠️ PUT **активного** (PUBLISHED) графа сервер проверяет как `publish`: валидатор, платные блоки, лимит блоков тарифа, платформа. Ошибка → `HTTP 422` со всеми `code@nodeId`, граф **не сохранён**, бот работает на прежней версии. Черновик (`DRAFT`) PUT сохраняет без проверок (кроме размера: больше 4 МБ → 422 `GRAPH_TOO_LARGE`), и на бота он не влияет: `publish_graph` черновика копирует его в отдельный `PUBLISHED`-граф (`publishedGraphId`), а сам черновик остаётся `DRAFT`. Всё равно прогоняй `validate.mjs` + `dry_run` заранее. Откат: версии активного не хранятся — опубликовать граф «🔙 Авто-бэкап» (или скопировать его содержимое обратно).
|
|
72
73
|
|
|
@@ -74,7 +75,7 @@ description: Собрать воронку (сценарий) бота для с
|
|
|
74
75
|
|
|
75
76
|
## Важные ограничения
|
|
76
77
|
- **Источник = текст** (этот режим). Если просят распознать с приватной Miro-доски — самый надёжный путь: CSV-экспорт из Miro; либо запуск залогиненного Chrome пользователя и съёмка экрана (headless WebGL-холст Miro не отдаёт). Это отдельный сценарий, не основной для этого скилла.
|
|
77
|
-
- **Авторизация MCP** — персональный токен (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, полный доступ). Если инструмент вернул «нет токена» или ошибку доступа — **вызови `setup`**, объясни пользователю шаги, попроси прислать токен и сохрани его через **`set_token`** (применяется сразу, без env/рестарта). Также работают env `RETENSY_MCP_TOKEN` и session-cookie.
|
|
78
|
+
- **Авторизация MCP** — персональный токен (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, с правами: `read`, `write:bots`, `write:sites`, `crm`, `broadcast`, `billing`, `destructive`; старые токены — полный доступ). Ошибка «Токену не хватает права «X»» — не сбой и не повод перевыпускать токен вслепую: скажи пользователю, какое право нужно, и попроси добавить его на `/bots/mcp-tokens` («Изменить права», действует сразу); для сборки воронки достаточно `read` + `write:bots`, но публикация сценария с `TRIGGER_SCHEDULE` (и включение цепочки без согласования) требует ещё `broadcast` — без него оставь черновик и скажи об этом пользователю. Если инструмент вернул «нет токена» или ошибку доступа — **вызови `setup`**, объясни пользователю шаги, попроси прислать токен и сохрани его через **`set_token`** (применяется сразу, без env/рестарта). Также работают env `RETENSY_MCP_TOKEN` и session-cookie.
|
|
78
79
|
- Бэкенд читает плоские поля `config.text`/`config.photoUrl`; редактор берёт текст из первой карточки `type:"text"`. Поэтому **всегда заполняй и `text`, и `cards`**.
|
|
79
80
|
- **Размер графа ≤ 4 МБ** (JSON `{nodes, edges, canvasMeta}` в UTF-8): больше — `HTTP 422 GRAPH_TOO_LARGE` на `update_graph`/`edit_graph_live`/`patch_graph`/`import_funnel`, и у черновика тоже; граф не сохранён. `validate.mjs` проверяет заранее.
|
|
80
81
|
- **Переменные подписчика:** строка ≤ 65 536 символов (длиннее обрезается), список/объект длиннее не сохраняется; все переменные одного подписчика вместе с именами ≤ 262 144 символов — запись сверх лимита не сохраняется. Большой ответ API не клади в переменную целиком — вытаскивай нужное через `extract`.
|
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.
|
|
30
|
+
const VERSION = "0.26.0";
|
|
31
31
|
/** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
|
|
32
32
|
const SITE_INITS = ["starter", "blank", "mini-landing"];
|
|
33
33
|
/** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
|
|
@@ -277,6 +277,8 @@ async function api(path_, { method = "GET", body, rawBody } = {}) {
|
|
|
277
277
|
throw new Error(`Доступ отклонён (HTTP 401). Токен невалиден, отозван или истёк.\n` +
|
|
278
278
|
`Создай новый на ${TOKENS_PAGE} и пришли мне — я сохраню через set_token.`);
|
|
279
279
|
}
|
|
280
|
+
const scopeErr = scopeError(res.status, data);
|
|
281
|
+
if (scopeErr) throw scopeErr;
|
|
280
282
|
if (res.status === 403) {
|
|
281
283
|
// 403 бэкенд отдаёт и на «не твой бот/граф/подключение» — это не всегда про токен.
|
|
282
284
|
const why = bodyReason(data);
|
|
@@ -289,6 +291,38 @@ async function api(path_, { method = "GET", body, rawBody } = {}) {
|
|
|
289
291
|
return data;
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
/** Описания прав токена — те же имена, что у бэкенда (PatScope). */
|
|
295
|
+
const SCOPE_TITLES = {
|
|
296
|
+
read: "чтение",
|
|
297
|
+
"write:bots": "изменение ботов и сценариев",
|
|
298
|
+
"write:sites": "изменение сайтов и лендингов",
|
|
299
|
+
crm: "работа с аудиторией (CRM)",
|
|
300
|
+
broadcast: "рассылки и сообщения подписчикам",
|
|
301
|
+
billing: "оплаты и платные ключи",
|
|
302
|
+
destructive: "удаление и выгрузка персональных данных",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 403 {error:"insufficient_scope"} — у токена нет нужного права (scopes проверяет сервер на каждом запросе).
|
|
307
|
+
* Это не «чужой объект» и не «токен сломан»: повторять запрос бессмысленно, нужен токен с этим правом.
|
|
308
|
+
*/
|
|
309
|
+
function scopeError(status, data) {
|
|
310
|
+
if (status !== 403 || !data || typeof data !== "object" || data.error !== "insufficient_scope") return null;
|
|
311
|
+
const scope = typeof data.requiredScope === "string" ? data.requiredScope : null;
|
|
312
|
+
const have = Array.isArray(data.tokenScopes) && data.tokenScopes.length ? ` Права токена сейчас: ${data.tokenScopes.join(", ")}.` : "";
|
|
313
|
+
const msg = scope
|
|
314
|
+
? `Токену не хватает права «${scope}» (${SCOPE_TITLES[scope] || scope}) — сервер отклонил запрос (HTTP 403).${have} ` +
|
|
315
|
+
`Это ограничение токена, а не ошибка: попроси владельца добавить право «${scope}» токену на ${TOKENS_PAGE} ` +
|
|
316
|
+
`(кнопка «Изменить права», действует сразу) или выпустить новый токен с этим правом. Повторять запрос без этого бесполезно.`
|
|
317
|
+
: `Токену не хватает прав: этот запрос недоступен токену с ограниченными правами (HTTP 403).${have} ` +
|
|
318
|
+
`Попроси владельца выдать токену «Полный доступ» на ${TOKENS_PAGE} или выполни действие в кабинете.`;
|
|
319
|
+
const err = new Error(msg);
|
|
320
|
+
err.status = 403;
|
|
321
|
+
err.data = data;
|
|
322
|
+
err.requiredScope = scope;
|
|
323
|
+
return err;
|
|
324
|
+
}
|
|
325
|
+
|
|
292
326
|
/** Причина отказа из тела ответа: {error}/{message} или строка. */
|
|
293
327
|
function bodyReason(data) {
|
|
294
328
|
if (data == null) return "";
|
|
@@ -365,6 +399,7 @@ async function uploadMedia({ filePath, url, filename }) {
|
|
|
365
399
|
const text = await res.text();
|
|
366
400
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
367
401
|
if (!res.ok) {
|
|
402
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
368
403
|
if (res.status === 401 || res.status === 403) throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
369
404
|
if (res.status === 402) throw new Error(`Лимит хранилища тарифа исчерпан (HTTP 402). Удали ненужные файлы (delete_file) или подними тариф: 🔗 ${SUBSCRIPTION_PAGE}`);
|
|
370
405
|
if (res.status === 413) throw new Error("Файл больше 50 МБ (HTTP 413) — лимит Telegram для видео/документов.");
|
|
@@ -407,6 +442,7 @@ async function uploadSiteAsset(siteId, { filePath, url }) {
|
|
|
407
442
|
const text = await res.text();
|
|
408
443
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
409
444
|
if (!res.ok) {
|
|
445
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
410
446
|
if (res.status === 401 || res.status === 403) throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
411
447
|
if (res.status === 402) throw new Error(`Лимит хранилища тарифа исчерпан (HTTP 402). Подними тариф: 🔗 ${SUBSCRIPTION_PAGE}`);
|
|
412
448
|
throw httpError("POST", `/api/bots/pages/${siteId}/upload`, res.status, data);
|
|
@@ -423,6 +459,7 @@ async function apiBinaryPost(path_, body) {
|
|
|
423
459
|
const text = await res.text().catch(() => "");
|
|
424
460
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
425
461
|
if (res.status === 401) throw new Error(`Доступ отклонён (HTTP 401). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
462
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
426
463
|
if (res.status === 403) throw new Error(`Доступ отклонён (HTTP 403)${bodyReason(data) ? `: ${bodyReason(data)}` : ""}.`);
|
|
427
464
|
if (res.status === 402) throw paymentError(data);
|
|
428
465
|
throw httpError("POST", path_, res.status, data);
|
|
@@ -480,6 +517,7 @@ async function migrationMultipart(path_, { filePath, platform, mapping }) {
|
|
|
480
517
|
const text = await res.text();
|
|
481
518
|
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
482
519
|
if (!res.ok) {
|
|
520
|
+
if (scopeError(res.status, data)) throw scopeError(res.status, data);
|
|
483
521
|
if (res.status === 401 || res.status === 403) throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
484
522
|
throw httpError("POST", path_, res.status, data);
|
|
485
523
|
}
|
|
@@ -986,7 +1024,8 @@ const TOOLS = [
|
|
|
986
1024
|
{ 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"] } },
|
|
987
1025
|
{ 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"] } },
|
|
988
1026
|
{ 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"] } },
|
|
989
|
-
{ name: "publish_graph", description: "Опубликовать граф. У бота ОДИН активный сценарий: публикация черновика заменяет содержимое активного (id живого графа сохраняется); прежняя активная версия, которой нет ни в одном черновике бота (правки вживую), сохраняется черновиком «… (была активной до дата время)». Вернёт publishedGraphId; при отказе проверок — ошибка HTTP 422 со всеми причинами построчно (code@nodeId: message). Сценарий-вебхук (источник WEBHOOK) этим инструментом не публикуется — HTTP 409, его публикуют в вебе.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
1027
|
+
{ name: "publish_graph", description: "Опубликовать граф. У бота ОДИН активный сценарий: публикация черновика заменяет содержимое активного (id живого графа сохраняется); прежняя активная версия, которой нет ни в одном черновике бота (правки вживую), сохраняется черновиком «… (была активной до дата время)». Вернёт publishedGraphId; при отказе проверок — ошибка HTTP 422 со всеми причинами построчно (code@nodeId: message). Сценарий-вебхук (источник WEBHOOK) этим инструментом не публикуется — HTTP 409, его публикуют в вебе. secondary:true — «опубликовать как ДОПОЛНИТЕЛЬНЫЙ сценарий»: граф становится живым НА МЕСТЕ (publishedGraphId = graphId, снимка нет) рядом с основным, основной сценарий и activeGraphId НЕ меняются; его запускают только VK-лендинги (targetGraphId) и ссылки link_create с targetGraphId. Те же проверки (422), у бота должен быть основной (SECONDARY_NO_MAIN), основной сам дополнительным не делается (SECONDARY_IS_MAIN); триггеры расписания/заявки с сайта/событий канала/комментариев/Instagram/вебхука — 422 SECONDARY_TRIGGER_UNSUPPORTED; режим decoupled — SECONDARY_UNSUPPORTED. У клона основного совпавшие id узлов при первой такой публикации меняются — перечитай граф. Снять — unpublish_graph.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, secondary: { type: "boolean", description: "true — дополнительный сценарий рядом с основным (для лендингов/ссылок); по умолчанию false — основной" } }, required: ["graphId"] } },
|
|
1028
|
+
{ name: "unpublish_graph", description: "Снять ДОПОЛНИТЕЛЬНЫЙ сценарий с публикации (POST /api/bots/graphs/{graphId}/unpublish): он снова черновик, лендинги и ссылки на него ведут в основной сценарий, подписчики в нём возвращаются в основной, его задержки отменяются. Основной сценарий так не снимается — HTTP 409.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
990
1029
|
{ 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"] } },
|
|
991
1030
|
{ name: "list_templates", description: "Список готовых шаблонов воронок (id, имя, описание). Можно стартовать граф из шаблона вместо сборки с нуля.", inputSchema: { type: "object", properties: {} } },
|
|
992
1031
|
{ 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"] } },
|
|
@@ -1042,11 +1081,11 @@ const TOOLS = [
|
|
|
1042
1081
|
{ name: "kb_reindex", description: "Переиндексировать документ-файл базы знаний из сохранённого оригинала (POST /api/bots/kb/{kbId}/docs/{docId}/reindex) — «Повторить» после ошибки. headerRow — необязательно, для табличных файлов: номер строки с шапкой (1..50), если автоопределение ошиблось. Только для источника FILE.", inputSchema: { type: "object", properties: { kbId: { type: "string" }, docId: { type: "string" }, headerRow: { type: "number" } }, required: ["kbId", "docId"] } },
|
|
1043
1082
|
{ name: "agent_unanswered", description: "Вопросы без ответа агента за период (GET /api/bots/agents/{agentId}/unanswered): days — 7|30|90 (по умолчанию 30). Группы вопросов, на которые агент не нашёл ответ в базе знаний — подсказка, что туда добавить. Read-only.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, days: { type: "number", enum: [7, 30, 90] } }, required: ["agentId"] } },
|
|
1044
1083
|
// ---- Боты ----
|
|
1045
|
-
{ name: "create_bot", description: "Подключить бота по токену (POST /api/bots): platform TELEGRAM (токен от @BotFather) или MAX (токен от MasterBot в MAX). Вебхук настраивается сам; name — отображаемое имя (иначе @username). Возвращает бота с id. Число ботов ограничено тарифом — HTTP 402 со ссылкой на смену тарифа. platform WEB — чат-виджет для сайта без токена (POST /api/bots/web): вернёт botId, key и snippet — код вставки на сайт (у существующего виджета — web_widget_snippet). platform INSTAGRAM по токену не подключается — только входом через Instagram (OAuth) в кабинете (/bots/instagram), нужен Instagram-аккаунт Business или Creator — инструмент вернёт ссылку на кабинет вместо ошибки. platform VK — сообщество как полноценный канал (Callback API, НЕ путать с vk_connect — тот для публикаций на стену): token — токен СООБЩЕСТВА (Управление сообществом → Работа с API → Создать ключ, доступ «Сообщения сообщества»), groupId — числовой id сообщества (обязателен). После подключения нужно вручную включить Callback API в настройках сообщества — см. webhookSecret в ответе и URL /vk/hooks/{botId} (подробный чек-лист у разработчика).", inputSchema: { type: "object", properties: { platform: { type: "string", enum: ["TELEGRAM", "MAX", "WEB", "INSTAGRAM", "VK"] }, token: { type: "string", description: "Токен бота/сообщества: 123456789:AA… (Telegram), токен MAX, или community token VK" }, groupId: { type: "string", description: "Только platform=VK: числовой id сообщества" }, name: { type: "string" } }, required: ["platform"] } },
|
|
1084
|
+
{ name: "create_bot", description: "Подключить бота по токену (POST /api/bots): platform TELEGRAM (токен от @BotFather) или MAX (токен от MasterBot в MAX). Вебхук настраивается сам; name — отображаемое имя (иначе @username). Возвращает бота с id. Число ботов ограничено тарифом — HTTP 402 со ссылкой на смену тарифа. platform WEB — чат-виджет для сайта без токена (POST /api/bots/web): вернёт botId, key и snippet — код вставки на сайт (у существующего виджета — web_widget_snippet). platform INSTAGRAM по токену не подключается — только входом через Instagram (OAuth) в кабинете (/bots/instagram), нужен Instagram-аккаунт Business или Creator — инструмент вернёт ссылку на кабинет вместо ошибки. platform VK — сообщество как полноценный канал (Callback API, НЕ путать с vk_connect — тот для публикаций на стену): token — токен СООБЩЕСТВА (Управление сообществом → Работа с API → Создать ключ, доступ «Сообщения сообщества»), groupId — числовой id сообщества (обязателен). После подключения нужно вручную включить Callback API в настройках сообщества — см. webhookSecret в ответе и URL /vk/hooks/{botId} (подробный чек-лист у разработчика). inactive (только TELEGRAM/MAX) — создать бота остановленным: токен проверяется безопасным getMe, вебхук/поллинг НЕ трогаются — живой вебхук бота в другом конструкторе (BotHelp и т.п.) не снимается. Нужно при переносе бота из другого сервиса: создай inactive:true, перенеси сценарии/подписчиков (migration_preview/migration_commit), потом bot_resume — это и есть переключение (cutover).", inputSchema: { type: "object", properties: { platform: { type: "string", enum: ["TELEGRAM", "MAX", "WEB", "INSTAGRAM", "VK"] }, token: { type: "string", description: "Токен бота/сообщества: 123456789:AA… (Telegram), токен MAX, или community token VK" }, groupId: { type: "string", description: "Только platform=VK: числовой id сообщества" }, name: { type: "string" }, inactive: { type: "boolean", description: "Только TELEGRAM/MAX: создать остановленным (перенос из другого конструктора) — включить потом через bot_resume" } }, required: ["platform"] } },
|
|
1046
1085
|
// ---- VK-лендинги (growth tools, Mini App) ----
|
|
1047
1086
|
{ name: "vk_landing_list", description: "Список VK-лендингов (Mini App, GET /api/growth/vk-landings): контент, сценарий-цель, авто-режим, готовые ссылки links.{base,auto}, статистика views/clicks/subscribes. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1048
1087
|
{ name: "vk_landing_get", description: "Один VK-лендинг по id (GET /api/growth/vk-landings/{id}). 404 — не найден/чужой.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
1049
|
-
{ name: "vk_landing_create", description: "Создать VK-лендинг (POST /api/growth/vk-landings): botId — VK-бот (create_bot platform VK), title обязателен, остальное опционально — subtitle, imageUrl, buttonText, targetGraphId (сценарий —
|
|
1088
|
+
{ name: "vk_landing_create", description: "Создать VK-лендинг (POST /api/growth/vk-landings): botId — VK-бот (create_bot platform VK), title обязателен, остальное опционально — subtitle, imageUrl, buttonText, targetGraphId (сценарий ЭТОГО бота из list_graphs, иначе HTTP 400: пусто или основной — подписчик идёт в основной сценарий по /start; опубликованный как ДОПОЛНИТЕЛЬНЫЙ (publish_graph secondary:true) — стартует в нём; черновик — пока основной; ответ показывает targetGraphStatus MAIN|SECONDARY|NOT_PUBLISHED|MISSING), tags (добавляются подписчику при переходе), autoSubscribe (true = &auto=1, подписка сразу при открытии), bothelpLandingId (маппинг старой ссылки BotHelp). Возвращает лендинг с готовыми ссылками links.base/links.auto вида vk.com/app<id>_-<groupId>#l=<id>[&auto=1] — публикуй их в сообществе.", inputSchema: { type: "object", properties: { botId: { type: "string" }, title: { type: "string" }, subtitle: { type: "string" }, imageUrl: { type: "string" }, buttonText: { type: "string" }, targetGraphId: { type: "string" }, tags: { type: "array", items: { type: "string" } }, autoSubscribe: { type: "boolean" }, bothelpLandingId: { type: "string" } }, required: ["botId", "title"] } },
|
|
1050
1089
|
{ name: "vk_landing_update", description: "Изменить VK-лендинг (PUT /api/growth/vk-landings/{id}) — те же поля, что vk_landing_create, полностью заменяют текущие (не частичный патч).", inputSchema: { type: "object", properties: { id: { type: "string" }, botId: { type: "string" }, title: { type: "string" }, subtitle: { type: "string" }, imageUrl: { type: "string" }, buttonText: { type: "string" }, targetGraphId: { type: "string" }, tags: { type: "array", items: { type: "string" } }, autoSubscribe: { type: "boolean" }, bothelpLandingId: { type: "string" } }, required: ["id", "botId", "title"] } },
|
|
1051
1090
|
{ name: "vk_landing_delete", description: "Удалить VK-лендинг (DELETE /api/growth/vk-landings/{id}) — ссылки перестают работать.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
1052
1091
|
{ name: "bot_stop", description: "Остановить бота (POST /api/bots/{botId}/stop): снимает вебхук, бот перестаёт отвечать, сценарии и подписчики сохраняются. Запуск обратно — bot_resume.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
@@ -1112,7 +1151,7 @@ const TOOLS = [
|
|
|
1112
1151
|
{ 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"] } },
|
|
1113
1152
|
{ 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"] } },
|
|
1114
1153
|
{ 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"] } },
|
|
1115
|
-
{ 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"] } },
|
|
1154
|
+
{ name: "link_create", description: "Создать стартовую ссылку бота (POST /api/bots/{botId}/links {name, targetNodeId?}): code для t.me/<бот>?start=<code>, счётчик стартов. targetNodeId — узел сценария, на который ведёт диплинк; для одного узла повторный вызов вернёт существующую ссылку. targetGraphId — дополнительный сценарий этого бота (publish_graph secondary:true), в котором стартует подписчик (targetNodeId тогда — узел в нём); не опубликован как дополнительный — ведёт в основной; чужой граф — HTTP 400.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" }, targetNodeId: { type: "string" }, targetGraphId: { type: "string" } }, required: ["botId"] } },
|
|
1116
1155
|
{ 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"] } },
|
|
1117
1156
|
{ name: "utm_sources", description: "UTM-источники подписчиков бота (GET /api/bots/{botId}/utm-sources): по каким меткам utm_* люди входили в сценарии и сколько их. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
1118
1157
|
{ 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"] } },
|
|
@@ -1330,7 +1369,8 @@ async function handleCall(params) {
|
|
|
1330
1369
|
return okResult(await api(`/api/bots/graphs/${a.graphId}/dry-run`, { method: "POST", body: { kind: a.kind, value, fromUsername: a.fromUsername, presetVariables: a.presetVariables, presetTags: a.presetTags } }));
|
|
1331
1370
|
}
|
|
1332
1371
|
case "publish_graph": {
|
|
1333
|
-
const
|
|
1372
|
+
const qs = a.secondary === true ? "?secondary=true" : "";
|
|
1373
|
+
const pub = await api(`/api/bots/graphs/${a.graphId}/publish${qs}`, { method: "POST" });
|
|
1334
1374
|
// errors[] приходит с HTTP 200, но для пользователя это «не получилось» — и самый
|
|
1335
1375
|
// ценный сигнал: видно, какого узла/возможности ему не хватило.
|
|
1336
1376
|
if (Array.isArray(pub?.errors) && pub.errors.length) {
|
|
@@ -1723,9 +1763,12 @@ async function handleCall(params) {
|
|
|
1723
1763
|
await api(`/api/bots/${botId}/channels/${channelId}`, { method: "DELETE" });
|
|
1724
1764
|
return okResult(`🗑️ Канал ${channelId} отключён от бота ${botId}.`);
|
|
1725
1765
|
}
|
|
1766
|
+
case "unpublish_graph":
|
|
1767
|
+
return okResult(await api(`/api/bots/graphs/${uuidArg(a.graphId, "graphId")}/unpublish`, { method: "POST" }));
|
|
1726
1768
|
case "link_create": {
|
|
1727
1769
|
const body = { name: a.name || undefined };
|
|
1728
1770
|
if (a.targetNodeId) body.targetNodeId = uuidArg(a.targetNodeId, "targetNodeId");
|
|
1771
|
+
if (a.targetGraphId) body.targetGraphId = uuidArg(a.targetGraphId, "targetGraphId");
|
|
1729
1772
|
return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/links`, { method: "POST", body }));
|
|
1730
1773
|
}
|
|
1731
1774
|
case "link_delete": {
|
|
@@ -1937,9 +1980,10 @@ async function handleCall(params) {
|
|
|
1937
1980
|
? "Нужен токен MAX-бота: создай бота у MasterBot в MAX и пришли токен — я подключу его (create_bot {platform:\"MAX\", token})."
|
|
1938
1981
|
: "Нужен токен Telegram-бота: открой https://t.me/BotFather → /newbot (или /token для существующего) и пришли токен вида 123456789:AA… — я подключу его (create_bot {platform:\"TELEGRAM\", token}).");
|
|
1939
1982
|
}
|
|
1983
|
+
const inactive = a.inactive === true; // перенос из другого конструктора: не трогать живой вебхук
|
|
1940
1984
|
let bot;
|
|
1941
1985
|
try {
|
|
1942
|
-
bot = await api("/api/bots", { method: "POST", body: { token, platform } });
|
|
1986
|
+
bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
|
|
1943
1987
|
} catch (e) {
|
|
1944
1988
|
// 400 приходит строкой-причиной (неверный токен, бот уже подключён) — её и показываем.
|
|
1945
1989
|
if (e.status === 400) throw new Error(`Бот не подключён: ${typeof e.data === "string" && e.data ? e.data : "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
|
|
@@ -1949,7 +1993,7 @@ async function handleCall(params) {
|
|
|
1949
1993
|
try { bot = await api(`/api/bots/${bot.id}`, { method: "PATCH", body: { name: a.name } }); }
|
|
1950
1994
|
catch (e) { return okResult({ ...bot, warning: `Бот подключён, но имя не задано: ${(e.message || "").split("\n")[0]}` }); }
|
|
1951
1995
|
}
|
|
1952
|
-
return okResult(bot);
|
|
1996
|
+
return okResult(inactive ? { ...bot, note: "Бот создан остановленным (как после bot_stop) — вебхук/поллинг не тронуты. Перенеси сценарии и подписчиков, затем вызови bot_resume для переключения (cutover)." } : bot);
|
|
1953
1997
|
}
|
|
1954
1998
|
case "vk_landing_list": return okResult(await api("/api/growth/vk-landings"));
|
|
1955
1999
|
case "vk_landing_get": return okResult(await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`));
|