@retensy/mcp 0.14.0 → 0.15.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/.claude-plugin/marketplace.json +1 -0
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -2
- package/package.json +2 -2
- package/skills/build-bot-funnel/SKILL.md +4 -3
- package/skills/build-bot-funnel/reference/schema.md +8 -0
- package/skills/build-bot-funnel/validate.mjs +6 -0
- package/skills/build-site/SKILL.md +3 -1
- package/src/index.mjs +47 -10
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"name": "retensy-mcp",
|
|
8
8
|
"source": "./",
|
|
9
|
+
"version": "0.15.0",
|
|
9
10
|
"description": "Сборка и публикация воронок ботов (Telegram/MAX, Retensy Bots) из описания, рассылки, подключение ботов и сервисов, сайтов из блоков + публикация статей блога в Markdown — через MCP + скилл."
|
|
10
11
|
}
|
|
11
12
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retensy-mcp",
|
|
3
3
|
"displayName": "Retensy MCP",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"description": "MCP-сервер + скилл для Retensy Bots: подключение ботов и сервисов, сборка и публикация воронок ботов (Telegram/MAX), рассылки, сайты из блоков (Zero-блок, дизайны, шаблоны, свои домены, заявки) и статьи блога в Markdown — всё одним токеном zmcp_.",
|
|
6
6
|
"author": { "name": "retensy", "url": "https://bots.retensy.com" },
|
|
7
7
|
"homepage": "https://bots.retensy.com/bots",
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
MCP-сервер (+ скилл для Claude Code) для **сборки и публикации воронок/автоматизаций ботов (Telegram, MAX и Instagram)** в сервисе [retensy `/bots`](https://bots.retensy.com/bots): из текстового описания → валидный граф сценария → заливка и публикация через API.
|
|
9
9
|
|
|
10
10
|
- 🤖 **Сценарии ботов**: `list_bots`, `list_graphs`, `list_channels`, `get_graph`, `create_graph`, `update_graph`, `edit_graph_live`, `patch_graph`, `dry_run`, `publish_graph`, `import_funnel`, `list_templates`, `create_graph_from_template`, `clone_graph`, `copy_graph`, `rename_graph`, `set_active_graph`, `delete_graph`, `upload_file`, `list_files`, `delete_file`, `graph_analytics`, `list_bot_users`, `list_links` (+ `setup`/`set_token`).
|
|
11
|
-
- 🔌 **Боты и сервисы**: `create_bot` (Telegram/MAX по токену), `bot_stop`/`bot_resume`, `connect_integration`/`disconnect_integration` (amoCRM, Битрикс24, GetCourse, Я.Метрика, ЮKassa; Google Таблицы — ссылкой на вход Google).
|
|
11
|
+
- 🔌 **Боты и сервисы**: `create_bot` (Telegram/MAX по токену), `bot_stop`/`bot_resume`, `connect_integration`/`disconnect_integration` (amoCRM, Битрикс24, GetCourse, Я.Метрика, ЮKassa; Google Таблицы — ссылкой на вход Google; креды хранятся зашифрованными и не возвращаются), `integration_catalog`/`integration_status`/`integration_test` (каталог Integration Core и живая проверка подключения), `channel_post` (пост в канал Telegram/MAX).
|
|
12
12
|
- 📣 **Рассылки**: `broadcast_preview` (размер аудитории), `broadcast_send` (сейчас или по расписанию, сразу по нескольким ботам; сообщения или запуск сценария), `broadcast_list`/`broadcast_get`/`broadcast_cancel`, повторяющиеся (`broadcast_recurring`), черновики (`broadcast_drafts`, `broadcast_duplicate`) + скилл `send-broadcast`.
|
|
13
13
|
- 🔗 **Ссылка вместо отказа**: что нельзя сделать через API (вход через Google/Facebook, оплата тарифа, вход в аккаунт) — инструмент возвращает прямую ссылку и одну строку, что сделать.
|
|
14
14
|
- 📝 **Статьи блога** (тот же токен `zmcp_…`): `article_publish`, `article_update`, `article_list`, `article_get` — публикация статей в Markdown (как README на GitHub) в раздел **/articles**.
|
|
@@ -103,6 +103,10 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
103
103
|
| `list_integrations()` | подключённые сервисы (amoCRM, Битрикс24, GetCourse, Я.Метрика, ЮKassa): `id` = `connectionId` для действий сценария |
|
|
104
104
|
| `connect_integration(provider, creds?, title?, connectionId?)` | подключить/обновить сервис; без кредов — какие поля нужны; Google Таблицы → ссылка входа Google, Instagram → ссылка на кабинет |
|
|
105
105
|
| `disconnect_integration(connectionId)` | удалить подключение |
|
|
106
|
+
| `integration_catalog()` | каталог Integration Core: поля подключения (`configSchema`) и действия (`actions[].kind`) сервисов |
|
|
107
|
+
| `integration_status(connectionId)` | статус подключения: `OK` / `NEEDS_REAUTH` / `ERROR` / `UNKNOWN`, последняя проверка и ошибка |
|
|
108
|
+
| `integration_test(connectionId)` | живая проверка ключа во внешнем сервисе от имени владельца (без побочных эффектов) |
|
|
109
|
+
| `channel_post(botId, chatId, text?, mediaUrl?)` | разовый пост в канал/группу Telegram или MAX (раздел «Публикации»); файл — из `upload_file` |
|
|
106
110
|
| `get_graph(graphId, [summary], [saveToFile])` | получить граф; `summary:true` — компактная сводка (id/type/title + рёбра), `saveToFile` — записать полный JSON на диск (для больших графов, чтобы не упереться в лимит токенов) |
|
|
107
111
|
| `create_graph(botId, name)` | создать пустой граф (DRAFT) |
|
|
108
112
|
| `update_graph(graphId, graphFile\|graph\|nodes,edges)` | залить узлы/рёбра (PUT); `graphFile` — путь к локальному JSON, граф не нужно слать инлайном |
|
|
@@ -134,7 +138,7 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
134
138
|
| `site_upload_asset(siteId, path\|url)` | картинка/видео в сайт → `assets/…` |
|
|
135
139
|
| `site_domains(siteId, action, host?, withWww?, domainId?)` | свои домены: list / add / check / remove (число — по тарифу) |
|
|
136
140
|
| `site_leads(siteId, page?, size?)` | заявки из форм (поля, UTM, статус доставки) |
|
|
137
|
-
| `site_lead_settings(siteId, settings?)` | куда доставлять заявки: бот уведомлений, почта, вебхук, вебхук-сценарий, amoCRM |
|
|
141
|
+
| `site_lead_settings(siteId, settings?)` | куда доставлять заявки: бот уведомлений, почта, вебхук, вебхук-сценарий, amoCRM, «Интеграция» (`coreDelivery {connectionId, kind, params}` из `coreConnections`) |
|
|
138
142
|
| `broadcast_list(botId?, group?, page?, size?)` | рассылки + счётчики разделов (черновики/запланированные/отправленные/повторы) |
|
|
139
143
|
| `broadcast_get(broadcastId)` | рассылка целиком: статус, счётчики, сообщения |
|
|
140
144
|
| `broadcast_preview(botIds, tagsAll?, tagsNone?)` | сколько подписчиков получат рассылку |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
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",
|
|
25
25
|
"validate": "node skills/build-bot-funnel/validate.mjs"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: build-bot-funnel
|
|
3
|
-
description: Собрать воронку (сценарий) бота для сервиса /bots из текстового описания — сгенерировать валидный граф формата retensy-bot-graph, проверить его и (через MCP bot-graph) залить в бота, прогнать dry-run и опубликовать. Поддерживает Telegram
|
|
3
|
+
description: Собрать воронку (сценарий) бота для сервиса /bots из текстового описания — сгенерировать валидный граф формата retensy-bot-graph, проверить его и (через MCP bot-graph) залить в бота, прогнать dry-run и опубликовать. Поддерживает Telegram и MAX (Instagram сейчас выключен в сервисе — IG-бота не подключить). Использовать, когда пользователь описывает воронку/прогрев/сценарий бота словами и просит «собрать», «сделать граф», «залить в бота», «опубликовать сценарий».
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Сборка воронки бота (retensy /bots)
|
|
7
7
|
|
|
8
|
-
Превращает **текстовое описание воронки** в валидный граф `retensy-bot-graph` и (опционально) публикует его через MCP-сервер `bot-graph`. Поддерживаются платформы **Telegram
|
|
8
|
+
Превращает **текстовое описание воронки** в валидный граф `retensy-bot-graph` и (опционально) публикует его через MCP-сервер `bot-graph`. Поддерживаются платформы **Telegram и MAX** — у каждой свои триггеры и ограничения, описанные в schema.md. **Instagram сейчас выключен в сервисе — IG-бота не подключить**; разделы о нём оставлены на будущее включение.
|
|
9
9
|
|
|
10
10
|
**Различия платформ (кратко):**
|
|
11
11
|
- **Telegram** — полный функционал: `/start` и другие команды, все типы узлов, SUBSCRIBED, кнопки-контакты, рассылки.
|
|
12
12
|
- **MAX** — те же узлы, но без SUBSCRIBED/reply-клавиатур (мягкие предупреждения при публикации, не блокируют).
|
|
13
|
-
- **Instagram** — ограниченный набор узлов; вход только через комментарий/директ/историю (нет `/start`); нет рассылок; DELAY не более 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER/CONTACT (CONTACT = ручной ввод номера, кнопки «Поделиться номером» в IG нет); онбординг через OAuth на странице `/bots/instagram` («Подключения» → Instagram), без токена бота.
|
|
13
|
+
- **Instagram** (сейчас выключен в сервисе — IG-бота не подключить) — ограниченный набор узлов; вход только через комментарий/директ/историю (нет `/start`); нет рассылок; DELAY не более 24ч; ASK_QUESTION только TEXT/EMAIL/PHONE/NUMBER/CONTACT (CONTACT = ручной ввод номера, кнопки «Поделиться номером» в IG нет); онбординг через OAuth на странице `/bots/instagram` («Подключения» → Instagram), без токена бота.
|
|
14
14
|
|
|
15
15
|
## Когда применять
|
|
16
16
|
Пользователь описывает сценарий бота словами: приветствие → подписка → вопрос с кнопками → ветки → задержки → вебинар и т.п. Либо просит залить/опубликовать готовую воронку.
|
|
@@ -39,6 +39,7 @@ description: Собрать воронку (сценарий) бота для с
|
|
|
39
39
|
- **не ждать** — ветку `paid` не рисуй, блок просто пришлёт счёт и пойдёт по `next`; выдать доступ потом сможет отдельный сценарий с триггером `TRIGGER_PAYMENT`.
|
|
40
40
|
Счёт обязан быть **последним** действием в блоке и только одним (`INVOICE_NOT_LAST`, `INVOICE_DUPLICATE`).
|
|
41
41
|
- **`TRIGGER_PAYMENT` («оплата прошла»)** — если счёт выставлял бот, подписчик определяется САМ (счёт помнит его в metadata платежа): можно сразу слать сообщение и вешать метку, разбирать ответ кассы не нужно. Если оплата пришла мимо бота — прогон headless, получателя задавай через `config.target`. Ставь фильтр `minAmount`/`descriptionContains`, иначе сценарий сработает на любую оплату в кассе.
|
|
42
|
+
- **`TRIGGER_SITE_FORM` («Заявка с сайта»)** — `{siteId, formId}`: посетитель отправил форму на ОПУБЛИКОВАННОМ сайте владельца (`site_list` → id сайта, id формы — из `site_get`). Пустой/отсутствующий `formId` = любая форма сайта. Поля формы — `{{var.<имя поля>}}`; имя, почта, телефон всегда доступны как `{{var.name}}`, `{{var.email}}`, `{{var.phone}}` (если поле названо иначе — находятся по подписи), UTM — `{{var.utm_source}}` и т.п. Чата с посетителем нет: узлы отправки работают только с явным `config.target`, CRM/таблицы/вебхуки — как обычно. Обычно это сценарий без бота (источник WEBHOOK, как вебхук-сценарий) — ему `TRIGGER_WEBHOOK` не нужен; можно и в сценарии бота. Чужой сайт → `SITE_FORM_FOREIGN_SITE`, без сайта → `SITE_FORM_NO_SITE`. Одна заявка — один запуск; `TRIGGER_ANY_UPDATE` на заявки не срабатывает.
|
|
42
43
|
|
|
43
44
|
4. **Проверь локально** перед заливкой:
|
|
44
45
|
```bash
|
|
@@ -72,6 +72,12 @@
|
|
|
72
72
|
если не подошёл ни один конкретный триггер. Удобен как «ничего не понял» / отладка.
|
|
73
73
|
- `TRIGGER_WEBHOOK` — `{}`, точка входа сценария с источником WEBHOOK (не бот). Такие сценарии
|
|
74
74
|
создаются в вебе; у графа бота этот триггер не сработает.
|
|
75
|
+
- `TRIGGER_SITE_FORM` — `{ "siteId": "<uuid сайта>", "formId": "<id формы>" }` — заявка с формы
|
|
76
|
+
опубликованного сайта владельца. `formId` пустой/нет = любая форма сайта. `siteId` обязателен
|
|
77
|
+
(`SITE_FORM_NO_SITE`), сайт должен быть свой (`SITE_FORM_FOREIGN_SITE`). Переменные: поля формы
|
|
78
|
+
`{{var.<имя>}}`, всегда `{{var.name}}`/`{{var.email}}`/`{{var.phone}}` (по имени поля, иначе по подписи),
|
|
79
|
+
UTM `{{var.utm_*}}`. Прогон без чата: узлы отправки — только с `config.target`. Работает и в сценарии
|
|
80
|
+
без бота (источник WEBHOOK — тогда `TRIGGER_WEBHOOK` не обязателен), и в сценарии бота; не для Instagram.
|
|
75
81
|
- ~~`TRIGGER_COMMENT`~~ — **мёртвый тип**: рантайм нигде не выставляет `event="comment"`, сработать
|
|
76
82
|
он не может. Убран из палитры редактора. Комментарии Instagram — `TRIGGER_IG_COMMENT`.
|
|
77
83
|
|
|
@@ -361,6 +367,8 @@ IG-боты не поддерживают команды (`/start`). Вход
|
|
|
361
367
|
|
|
362
368
|
## Платформа Instagram
|
|
363
369
|
|
|
370
|
+
> **Сейчас Instagram выключен в сервисе:** IG-бота не подключить, страница `/bots/instagram` ведёт на `/bots`. Раздел оставлен на будущее включение.
|
|
371
|
+
|
|
364
372
|
IG-боты подключаются через OAuth на странице **`/bots/instagram`** (раздел «Подключения» → карточка
|
|
365
373
|
Instagram; прежний раздел «Инструменты роста» / `/growth` расформирован и редиректит) — **без вставки токена вручную**; у IG нет персонального бот-токена. После OAuth бот получает доступ к Messaging API через привязанный Instagram Business/Creator-аккаунт.
|
|
366
374
|
|
|
@@ -213,6 +213,12 @@ for (const n of nodes) {
|
|
|
213
213
|
if (!blank(f.status) && ev !== "chat_member" && ev !== "my_chat_member") warns.push(`${who}: фильтр по статусу работает только у chat_member/my_chat_member.`);
|
|
214
214
|
break;
|
|
215
215
|
}
|
|
216
|
+
case "TRIGGER_SITE_FORM":
|
|
217
|
+
// Бэкенд (GraphValidator.validateSiteFormTrigger): siteId обязателен и должен быть UUID своего сайта.
|
|
218
|
+
if (blank(c.siteId)) errors.push(`SITE_FORM_NO_SITE: ${who} — нужен siteId (id сайта из site_list).`);
|
|
219
|
+
else if (!UUID_RE.test(String(c.siteId).trim())) errors.push(`SITE_FORM_NO_SITE: ${who} — siteId «${c.siteId}» не UUID сайта (site_list).`);
|
|
220
|
+
if (c.formId != null && typeof c.formId !== "string") errors.push(`${who}: formId — строка (id формы из site_get) или пусто = любая форма.`);
|
|
221
|
+
break;
|
|
216
222
|
case "TRIGGER_COMMENT":
|
|
217
223
|
errors.push(`TRIGGER_COMMENT_DEAD: ${who} — этот тип триггера не сработает никогда (рантайм не выставляет event=comment). Для Instagram используй TRIGGER_IG_COMMENT.`);
|
|
218
224
|
break;
|
|
@@ -19,7 +19,9 @@ description: Собрать сайт или лендинг из блоков в
|
|
|
19
19
|
4. Картинки — `site_upload_asset {siteId, path|url}` → `assets/…` в поля `image`, `logo`, `icon`, `style.bg.image`.
|
|
20
20
|
5. `site_get` — проверь модель, `site_publish` — сайт открыт по `url`.
|
|
21
21
|
6. После публикации: заявки — `site_leads`, куда их слать — `site_lead_settings` (бот уведомлений, почта, вебхук,
|
|
22
|
-
вебхук-сценарий бота, amoCRM
|
|
22
|
+
вебхук-сценарий бота, amoCRM, «Интеграция» — `coreDelivery {connectionId, kind, params?}`: любое подключение из
|
|
23
|
+
`coreConnections`, действие — `kind` из `integration_catalog`; запуск автоматизаций по заявке — сценарий с триггером
|
|
24
|
+
`TRIGGER_SITE_FORM {siteId, formId}`, скилл build-bot-funnel); свой домен — `site_domains` (`add` → пользователь ставит A-запись на `dnsTarget`
|
|
23
25
|
→ `check`); неудачная публикация — `site_rollback {revision}` из `site_get → versions[]`.
|
|
24
26
|
|
|
25
27
|
## Правила модели
|
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.15.0";
|
|
31
31
|
const PKG_NAME = "@retensy/mcp";
|
|
32
32
|
const BASE = (process.env.RETENSY_BASE_URL || "https://bots.retensy.com").replace(/\/+$/, "");
|
|
33
33
|
const CONFIG_DIR = path.join(os.homedir(), ".retensy-bot-graph");
|
|
@@ -542,8 +542,22 @@ function qs(params) {
|
|
|
542
542
|
// ============================================================================
|
|
543
543
|
// Подключения сервисов
|
|
544
544
|
// ============================================================================
|
|
545
|
-
|
|
546
|
-
const
|
|
545
|
+
/** IA v2: каталог и подключения — раздел «Интеграции». Старые /bots/connect|integrations остаются в кабинете (Instagram — там). */
|
|
546
|
+
const CONNECT_PAGE = `${BASE}/integrations`;
|
|
547
|
+
const INTEGRATIONS_PAGE = CONNECT_PAGE;
|
|
548
|
+
const LEGACY_CONNECT_PAGE = `${BASE}/bots/connect`;
|
|
549
|
+
/** Ключи с секретами: бэкенд их не отдаёт (только маска hint), но вычищаем и здесь — секрет не должен попасть в вывод. */
|
|
550
|
+
const SECRET_OUT_RX = /^(creds|credsEnc|credentials|password|api_?key|.*secret(key)?|.*token)$/i;
|
|
551
|
+
function withoutSecrets(v) {
|
|
552
|
+
if (Array.isArray(v)) return v.map(withoutSecrets);
|
|
553
|
+
if (!v || typeof v !== "object") return v;
|
|
554
|
+
const out = {};
|
|
555
|
+
for (const [k, x] of Object.entries(v)) {
|
|
556
|
+
if (SECRET_OUT_RX.test(k) && typeof x !== "boolean") continue; // secret:true в схеме каталога — флаг, не секрет
|
|
557
|
+
out[k] = withoutSecrets(x);
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
547
561
|
/** Поля кредов — как форма кабинета (IntegrationsPage.tsx PROVIDER_FIELDS). */
|
|
548
562
|
const PROVIDER_FIELDS = {
|
|
549
563
|
AMOCRM: { name: "amoCRM", fields: { subdomain: "поддомен: acme из acme.amocrm.ru", longToken: "долгосрочный токен: amoCRM → Интеграции → ваша интеграция → Ключи и доступы" } },
|
|
@@ -563,7 +577,7 @@ const normProvider = (p) => PROVIDER_ALIASES[String(p || "").trim().toLowerCase(
|
|
|
563
577
|
|
|
564
578
|
/** Instagram подключается только входом через Facebook (OAuth) и сейчас выключен в сервисе (instagram.enabled). */
|
|
565
579
|
function instagramAnswer() {
|
|
566
|
-
return linkResult("Instagram: подключение через вход Facebook (OAuth) — сейчас выключено в сервисе",
|
|
580
|
+
return linkResult("Instagram: подключение через вход Facebook (OAuth) — сейчас выключено в сервисе", LEGACY_CONNECT_PAGE,
|
|
567
581
|
"Instagram-аккаунт нельзя подключить по API или токену: только входом через Facebook в кабинете. Сейчас подключение Instagram в retensy выключено (страница /bots/instagram ведёт на список ботов). Открой каталог подключений по ссылке — когда Instagram включат, он появится там. Пока доступны Telegram и MAX (create_bot).");
|
|
568
582
|
}
|
|
569
583
|
|
|
@@ -573,6 +587,10 @@ const TOOLS = [
|
|
|
573
587
|
{ name: "list_bots", description: "Список ботов пользователя (id, имя, статус).", inputSchema: { type: "object", properties: {} } },
|
|
574
588
|
{ name: "list_graphs", description: "Список сценариев САМОГО бота (без узлов). Вебхук-сценарии, которые лишь отвечают через этого бота, сюда не входят — их публикуют в вебе, в «Сценариях» автора.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
575
589
|
{ name: "list_channels", description: "Список каналов/групп, подключённых к боту (chatId, title, type, статус бота, дата). chatId — числовой id для условия SUBSCRIBED («Подписан на канал»).", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
590
|
+
{ name: "integration_catalog", description: "Каталог сервисов Integration Core (GET /api/integrations/catalog): [{provider, name, category, authType, configSchema: [{key, label, hint, secret}] — поля подключения, actions: [{kind, label, inputs}] — действия для сценария и coreDelivery, healthCheck — умеет ли integration_test}]. 404 — Integration Core выключен в сервисе. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
591
|
+
{ name: "integration_status", description: "Статус подключения (GET /api/integrations/{id}/status): {status: UNKNOWN | OK | NEEDS_REAUTH (ключ отозван/устарел — обнови через connect_integration) | ERROR, lastCheckedAt, lastError, supported}. Только своё подключение (чужое — 403). Read-only, внешний сервис не вызывает.", inputSchema: { type: "object", properties: { connectionId: { type: "string", description: "id из list_integrations" } }, required: ["connectionId"] } },
|
|
592
|
+
{ name: "integration_test", description: "Проверить подключение (POST /api/integrations/{id}/test): выполняет ЖИВУЮ проверку ключа во внешнем сервисе от имени владельца (без побочных эффектов — ничего не создаёт) и обновляет статус. Ответ как у integration_status; supported:false — сервис проверку не умеет. Только своё подключение (чужое — 403); 404 — Integration Core выключен.", inputSchema: { type: "object", properties: { connectionId: { type: "string", description: "id из list_integrations" } }, required: ["connectionId"] } },
|
|
593
|
+
{ name: "channel_post", description: "Разовый пост от имени бота в канал/группу Telegram или MAX (раздел «Публикации», POST /api/bots/{botId}/linked-chats/{chatId}/post). Бот должен быть администратором канала (list_channels). text и/или mediaUrl — файл из upload_file (тип фото/видео/документ берётся из файла; чужие ссылки не принимаются). Лимиты: Telegram — 4096 символов текста, 1024 подписи к файлу; MAX — 4000. Ответ {ok, messageId?}. Публикует сразу — подтверди текст с пользователем.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "number", description: "chatId из list_channels" }, text: { type: "string" }, mediaUrl: { type: "string", description: "url из upload_file" } }, required: ["botId", "chatId"] } },
|
|
576
594
|
{ name: "list_integrations", description: "Список подключённых сервисов пользователя (GET /api/bots/integrations): {id, provider, title, hint, createdAt}. **id отсюда — это `connectionId`**, обязательное поле действий amocrm_send/amocrm_update/bitrix24_call/getcourse_send/getcourse_order/yametrika_event. Без него действие упадёт «не выбрано подключение». Креды не отдаются — только маскированный hint. Подключить новый — connect_integration. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
577
595
|
{ name: "get_graph", description: "Получить граф по graphId. Для БОЛЬШИХ графов (десятки узлов JSON может превысить лимит токенов) используй summary:true (компактная сводка: id/type/title/позиции + рёбра) или saveToFile (записать полный граф на диск и вернуть сводку+путь — потом правь файл и заливай через update_graph/edit_graph_live с graphFile).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, summary: { type: "boolean", description: "true = вернуть компактную сводку без объёмных text/cards/buttons" }, saveToFile: { type: "string", description: "Путь: записать полный граф (JSON) на диск, вернуть сводку + путь" } }, required: ["graphId"] } },
|
|
578
596
|
{ name: "create_graph", description: "Создать пустой граф (DRAFT) в боте. Возвращает граф с id.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
|
|
@@ -615,7 +633,7 @@ const TOOLS = [
|
|
|
615
633
|
{ name: "site_upload_asset", description: "Загрузить картинку/видео в сайт (POST /api/bots/pages/{siteId}/upload, папка assets). Передай path (локальный файл) ИЛИ url. Возвращает asset — строку вида assets/<имя> для полей image/logo/icon/style.bg.image.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, path: { type: "string" }, url: { type: "string" } }, required: ["siteId"] } },
|
|
616
634
|
{ name: "site_rollback", description: "Вернуть прошлую публикацию сайта (POST /api/bots/pages/{siteId}/publish/rollback): revision — номер из истории публикаций (site_get → versions[]). Черновик заменяется этой версией и сразу публикуется. Возвращает publishedRevision и url.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, revision: { type: "number" } }, required: ["siteId", "revision"] } },
|
|
617
635
|
{ name: "site_domains", description: "Свои домены сайта (/api/bots/pages/{siteId}/domains). action: list — домены, статусы и dnsTarget (IP для A-записи); add {host, withWww?} — привязать (withWww у корневого домена добавляет www-пару); check {domainId} — перепроверить DNS и сертификат; remove {domainId} — отвязать. Число доменов ограничено тарифом (HTTP 402 с upgradeUrl). Каждое действие возвращает актуальный список.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, action: { type: "string", enum: ["list", "add", "check", "remove"] }, host: { type: "string" }, withWww: { type: "boolean" }, domainId: { type: "string" } }, required: ["siteId", "action"] } },
|
|
618
|
-
{ name: "site_lead_settings", description: "Куда доставлять заявки из форм сайта (/api/bots/pages/{siteId}/lead-settings). Без settings — прочитать:
|
|
636
|
+
{ name: "site_lead_settings", description: "Куда доставлять заявки из форм сайта (/api/bots/pages/{siteId}/lead-settings). Без settings — прочитать: {settings, scenarios (вебхук-сценарии), amoConnections, coreConnections: [{id, name, provider}] — подключения для доставки «Интеграция», без кредов}. С settings — сохранить ЦЕЛИКОМ (сначала прочитай и поменяй нужное): {notifyBot: в бот уведомлений из профиля, notifyEmail: письмо на почту аккаунта, webhookUrl?: POST JSON на ваш адрес, scenarioId?: вебхук-сценарий, который запускает заявка, amoConnectionId?: сделка в amoCRM, coreDelivery?: {connectionId: id из coreConnections, kind: действие сервиса из integration_catalog (actions[].kind, например amocrm_send), params?: {поле действия: шаблон}} | null}. Пустые params заполнятся из заявки (имя, телефон, почта, текст); в шаблонах — {{var.name}}, {{var.phone}}, {{var.email}}, {{var.<имя поля>}}, {{var.lead_text}} (текст заявки). Чужое подключение или неизвестный kind — 400. Запуск сценариев по заявке — триггер TRIGGER_SITE_FORM (скилл build-bot-funnel).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, settings: { type: "object", properties: { notifyBot: { type: "boolean" }, notifyEmail: { type: "boolean" }, webhookUrl: { type: "string" }, scenarioId: { type: "string" }, amoConnectionId: { type: "string" }, coreDelivery: { type: ["object", "null"], properties: { connectionId: { type: "string" }, kind: { type: "string" }, params: { type: "object", additionalProperties: { type: "string" } } } } } } }, required: ["siteId"] } },
|
|
619
637
|
{ name: "site_leads", description: "Заявки из форм сайта (GET /api/bots/pages/{siteId}/leads): поля, UTM, статус доставки. page (с 0), size (до 100). Read-only.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["siteId"] } },
|
|
620
638
|
{ name: "article_list", description: "Список СВОИХ статей блога retensy (GET /api/articles/my): id, slug, title, viewCount, даты. id нужен для article_update, slug — публичный адрес /articles/{slug}. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
621
639
|
{ name: "article_get", description: "Получить статью блога по slug (GET /api/articles/by-slug/{slug}) — публичное чтение, в т.ч. чужие. Возвращает title, content (Markdown), excerpt, coverImage, viewCount.", inputSchema: { type: "object", properties: { slug: { type: "string", description: "slug статьи (часть адреса /articles/{slug})" } }, required: ["slug"] } },
|
|
@@ -626,7 +644,7 @@ const TOOLS = [
|
|
|
626
644
|
{ name: "bot_stop", description: "Остановить бота (POST /api/bots/{botId}/stop): снимает вебхук, бот перестаёт отвечать, сценарии и подписчики сохраняются. Запуск обратно — bot_resume.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
627
645
|
{ name: "bot_resume", description: "Запустить остановленного бота или бота, приостановленного лимитом тарифа (POST /api/bots/{botId}/resume). Если лимит ботов тарифа исчерпан — HTTP 402 со ссылкой на смену тарифа.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
628
646
|
// ---- Подключения ----
|
|
629
|
-
{ name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Креды
|
|
647
|
+
{ name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Креды хранятся в сервисе зашифрованными и НИКОГДА не возвращаются — ни здесь, ни в list_integrations (только маска hint); в отчёты не попадают. Проверить ключ после подключения — integration_test.", inputSchema: { type: "object", properties: { provider: { type: "string", description: "AMOCRM | BITRIX24 | GETCOURSE | YAMETRIKA | YOOKASSA | GOOGLE_SHEETS | INSTAGRAM" }, title: { type: "string", description: "Название подключения в кабинете (например «amoCRM продажи»)" }, creds: { type: "object", description: "Поля провайдера, см. описание" }, connectionId: { type: "string", description: "id существующего подключения (list_integrations) — обновить его" } }, required: ["provider"] } },
|
|
630
648
|
{ name: "disconnect_integration", description: "Удалить подключение сервиса по id из list_integrations (DELETE /api/bots/integrations/{id}). Действия сценария с этим connectionId перестанут работать.", inputSchema: { type: "object", properties: { connectionId: { type: "string" } }, required: ["connectionId"] } },
|
|
631
649
|
// ---- Рассылки ----
|
|
632
650
|
{ name: "broadcast_list", description: "Рассылки. Без botId — по всем ботам постранично (GET /api/bots/broadcasts): {counts: {drafts, scheduled, sent, recurring}, page: {content: [{id, botId, botUsername, name, status, direct, totalJobs, sentJobs, failedJobs, skippedByQuota, scheduledAt, createdAt}], totalElements…}}; group: scheduled (ещё не начали) | sent (идут/завершены). С botId — полная история одного бота. status: EXPANDING/MATERIALIZING/READY (ждёт) → RUNNING → DONE | CANCELLING → CANCELLED | FAILED. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, group: { type: "string", enum: ["scheduled", "sent"] }, page: { type: "number" }, size: { type: "number", description: "до 100, по умолчанию 20" } } } },
|
|
@@ -677,7 +695,26 @@ async function handleCall(params) {
|
|
|
677
695
|
case "list_bots": return okResult(await api("/api/bots"));
|
|
678
696
|
case "list_graphs": return okResult(await api(`/api/bots/${a.botId}/graphs`));
|
|
679
697
|
case "list_channels": return okResult(await api(`/api/bots/${a.botId}/linked-chats`));
|
|
680
|
-
case "list_integrations": return okResult(await api("/api/bots/integrations"));
|
|
698
|
+
case "list_integrations": return okResult(withoutSecrets(await api("/api/bots/integrations")));
|
|
699
|
+
case "integration_catalog": return okResult(withoutSecrets(await api("/api/integrations/catalog")));
|
|
700
|
+
case "integration_status":
|
|
701
|
+
case "integration_test": {
|
|
702
|
+
const id = String(a.connectionId || "").trim();
|
|
703
|
+
if (!id) throw new Error("Передай connectionId — id подключения из list_integrations.");
|
|
704
|
+
const live = params.name === "integration_test";
|
|
705
|
+
const p_ = `/api/integrations/${encodeURIComponent(id)}/${live ? "test" : "status"}`;
|
|
706
|
+
return okResult(withoutSecrets(await api(p_, live ? { method: "POST" } : undefined)));
|
|
707
|
+
}
|
|
708
|
+
case "channel_post": {
|
|
709
|
+
if (!a.botId) throw new Error("Передай botId (list_bots).");
|
|
710
|
+
const chatId = Number(a.chatId);
|
|
711
|
+
if (!Number.isSafeInteger(chatId)) throw new Error("chatId — числовой id канала из list_channels (например -1001234567890).");
|
|
712
|
+
const text = a.text == null ? "" : String(a.text);
|
|
713
|
+
if (!text.trim() && !a.mediaUrl) throw new Error("Пустой пост: передай text и/или mediaUrl (файл из upload_file).");
|
|
714
|
+
const body = { text };
|
|
715
|
+
if (a.mediaUrl) body.mediaUrl = String(a.mediaUrl);
|
|
716
|
+
return okResult(await api(`/api/bots/${encodeURIComponent(a.botId)}/linked-chats/${chatId}/post`, { method: "POST", body }));
|
|
717
|
+
}
|
|
681
718
|
case "get_graph": {
|
|
682
719
|
const g = await api(`/api/bots/graphs/${a.graphId}`);
|
|
683
720
|
if (a.saveToFile) {
|
|
@@ -891,8 +928,8 @@ async function handleCall(params) {
|
|
|
891
928
|
}
|
|
892
929
|
case "site_lead_settings": {
|
|
893
930
|
const p_ = `/api/bots/pages/${a.siteId}/lead-settings`;
|
|
894
|
-
if (a.settings == null) return okResult(await api(p_));
|
|
895
|
-
return okResult(await api(p_, { method: "PUT", body: a.settings }));
|
|
931
|
+
if (a.settings == null) return okResult(withoutSecrets(await api(p_)));
|
|
932
|
+
return okResult(withoutSecrets(await api(p_, { method: "PUT", body: a.settings })));
|
|
896
933
|
}
|
|
897
934
|
case "article_list": return okResult(await api("/api/articles/my"));
|
|
898
935
|
case "article_get": return okResult(await api(`/api/articles/by-slug/${encodeURIComponent(a.slug)}`));
|
|
@@ -968,7 +1005,7 @@ async function handleCall(params) {
|
|
|
968
1005
|
const saved = a.connectionId
|
|
969
1006
|
? await api(`/api/bots/integrations/${a.connectionId}`, { method: "PUT", body: { title: a.title, creds: Object.keys(creds).length ? creds : undefined } })
|
|
970
1007
|
: await api("/api/bots/integrations", { method: "POST", body: { provider, title, creds } });
|
|
971
|
-
return okResult({ connected: true, connectionId: saved?.id, ...saved, hint: "connectionId ставь в действия сценария (amocrm_send, bitrix24_call, getcourse_send, yametrika_event, оплата ЮKassa) и в site_lead_settings
|
|
1008
|
+
return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (amocrm_send, bitrix24_call, getcourse_send, yametrika_event, оплата ЮKassa) и в site_lead_settings (amoConnectionId или coreDelivery.connectionId). Проверить ключ — integration_test." });
|
|
972
1009
|
} catch (e) {
|
|
973
1010
|
if (e.status === 400) throw new Error(`${spec.name} не подключён: ${bodyReason(e.data) || "креды не приняты"}. Проверь поля: ${Object.entries(spec.fields).map(([k, v]) => `${k} — ${v}`).join("; ")}.`);
|
|
974
1011
|
throw e;
|