@retensy/mcp 0.25.1 → 0.26.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 +40 -3
- package/package.json +2 -2
- package/skills/build-bot-funnel/SKILL.md +1 -1
- package/src/index.mjs +43 -4
package/README.md
CHANGED
|
@@ -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`.
|
|
@@ -433,7 +470,7 @@ RETENSY_MCP_TOKEN=zmcp_... node src/index.mjs # стартует stdio MCP-с
|
|
|
433
470
|
|
|
434
471
|
## Безопасность
|
|
435
472
|
|
|
436
|
-
Токен = доступ к аккаунту по API. Не коммить
|
|
473
|
+
Токен = доступ к аккаунту по API в пределах его прав. Выдавай агенту минимально нужные права (для знакомства — «Только чтение»). Не коммить токен; держи в `env`. В конфигах храни ссылку `${RETENSY_MCP_TOKEN}`, не само значение.
|
|
437
474
|
|
|
438
475
|
## Лицензия
|
|
439
476
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|
|
25
25
|
"validate": "node skills/build-bot-funnel/validate.mjs"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
@@ -74,7 +74,7 @@ description: Собрать воронку (сценарий) бота для с
|
|
|
74
74
|
|
|
75
75
|
## Важные ограничения
|
|
76
76
|
- **Источник = текст** (этот режим). Если просят распознать с приватной Miro-доски — самый надёжный путь: CSV-экспорт из Miro; либо запуск залогиненного Chrome пользователя и съёмка экрана (headless WebGL-холст Miro не отдаёт). Это отдельный сценарий, не основной для этого скилла.
|
|
77
|
-
- **Авторизация MCP** — персональный токен (создаётся в вебе на `/bots/mcp-tokens`, формат `zmcp_…`, полный доступ). Если инструмент вернул «нет токена» или ошибку доступа — **вызови `setup`**, объясни пользователю шаги, попроси прислать токен и сохрани его через **`set_token`** (применяется сразу, без env/рестарта). Также работают env `RETENSY_MCP_TOKEN` и session-cookie.
|
|
77
|
+
- **Авторизация 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
78
|
- Бэкенд читает плоские поля `config.text`/`config.photoUrl`; редактор берёт текст из первой карточки `type:"text"`. Поэтому **всегда заполняй и `text`, и `cards`**.
|
|
79
79
|
- **Размер графа ≤ 4 МБ** (JSON `{nodes, edges, canvasMeta}` в UTF-8): больше — `HTTP 422 GRAPH_TOO_LARGE` на `update_graph`/`edit_graph_live`/`patch_graph`/`import_funnel`, и у черновика тоже; граф не сохранён. `validate.mjs` проверяет заранее.
|
|
80
80
|
- **Переменные подписчика:** строка ≤ 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
|
}
|
|
@@ -1042,7 +1080,7 @@ const TOOLS = [
|
|
|
1042
1080
|
{ 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
1081
|
{ 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
1082
|
// ---- Боты ----
|
|
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"] } },
|
|
1083
|
+
{ 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
1084
|
// ---- VK-лендинги (growth tools, Mini App) ----
|
|
1047
1085
|
{ 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
1086
|
{ name: "vk_landing_get", description: "Один VK-лендинг по id (GET /api/growth/vk-landings/{id}). 404 — не найден/чужой.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
@@ -1937,9 +1975,10 @@ async function handleCall(params) {
|
|
|
1937
1975
|
? "Нужен токен MAX-бота: создай бота у MasterBot в MAX и пришли токен — я подключу его (create_bot {platform:\"MAX\", token})."
|
|
1938
1976
|
: "Нужен токен Telegram-бота: открой https://t.me/BotFather → /newbot (или /token для существующего) и пришли токен вида 123456789:AA… — я подключу его (create_bot {platform:\"TELEGRAM\", token}).");
|
|
1939
1977
|
}
|
|
1978
|
+
const inactive = a.inactive === true; // перенос из другого конструктора: не трогать живой вебхук
|
|
1940
1979
|
let bot;
|
|
1941
1980
|
try {
|
|
1942
|
-
bot = await api("/api/bots", { method: "POST", body: { token, platform } });
|
|
1981
|
+
bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
|
|
1943
1982
|
} catch (e) {
|
|
1944
1983
|
// 400 приходит строкой-причиной (неверный токен, бот уже подключён) — её и показываем.
|
|
1945
1984
|
if (e.status === 400) throw new Error(`Бот не подключён: ${typeof e.data === "string" && e.data ? e.data : "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
|
|
@@ -1949,7 +1988,7 @@ async function handleCall(params) {
|
|
|
1949
1988
|
try { bot = await api(`/api/bots/${bot.id}`, { method: "PATCH", body: { name: a.name } }); }
|
|
1950
1989
|
catch (e) { return okResult({ ...bot, warning: `Бот подключён, но имя не задано: ${(e.message || "").split("\n")[0]}` }); }
|
|
1951
1990
|
}
|
|
1952
|
-
return okResult(bot);
|
|
1991
|
+
return okResult(inactive ? { ...bot, note: "Бот создан остановленным (как после bot_stop) — вебхук/поллинг не тронуты. Перенеси сценарии и подписчиков, затем вызови bot_resume для переключения (cutover)." } : bot);
|
|
1953
1992
|
}
|
|
1954
1993
|
case "vk_landing_list": return okResult(await api("/api/growth/vk-landings"));
|
|
1955
1994
|
case "vk_landing_get": return okResult(await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`));
|