@retensy/mcp 0.19.0 → 0.20.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 CHANGED
@@ -172,23 +172,29 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
172
172
  | `bot_user_get(botId, chatId)` | карточка подписчика: теги, переменные, ai_summary |
173
173
  | `bot_user_runs(botId, chatId)` | журнал запусков с шагами — проверка, что сценарий реально выполнил действия (CRM, уведомление, HTTP) |
174
174
  | `dialog_messages(botId, chatId)` / `dialog_reply(botId, chatId, text)` | переписка с подписчиком / ответ оператора (уходит реальному человеку) |
175
- | `dialog_handoff(botId, chatId, active)` | передать диалог оператору (бот и ИИ молчат) / вернуть боту ¹ |
175
+ | `dialog_handoff(botId, chatId, active)` | передать диалог оператору (бот и ИИ молчат) / вернуть боту |
176
176
  | `bot_users_import(botId, rows)` | добавить подписчикам метки и новые поля (`rows: [{chatId, tags?, variables?}]`; существующие поля не перезаписываются) |
177
177
  | `bot_runs(botId` или `runId)` | журнал прогонов бота / один прогон с шагами (статус `PARTIAL` — «завершён с ошибками») |
178
- | `scenario_runs(graphId, page?, size?)` | журнал прогонов одного сценария, включая headless (вебхук, заявки сайта, `TRIGGER_SCHEDULE`) ¹ |
178
+ | `scenario_runs(graphId, page?, size?)` | журнал прогонов одного сценария, включая headless (вебхук, заявки сайта, `TRIGGER_SCHEDULE`) |
179
179
  | `bot_delete(botId, confirm:true)` | удалить бота навсегда (только по явной просьбе) |
180
180
  | `web_widget_settings(botId, settings?)` | вид чат-виджета: прочитать / изменить (поля накладываются на текущие) |
181
181
  | `integration_update(connectionId, title?, creds?)` | переименовать подключение или заменить ключи |
182
- | `integration_calls(connectionId?, ok?, limit?)` | журнал вызовов внешних сервисов из сценариев (ошибки, попытки, runId) ¹ |
183
- | `site_lead_status(siteId, leadId, status)` | статус заявки сайта: `NEW`\|`IN_PROGRESS`\|`DONE`\|`REJECTED` (фильтр — `site_leads(status)`) ¹ |
184
-
185
- | `booking_calendar_list()` / `booking_calendar_get(calendarId)` | календари записи (id → `calendarId` действий сценария `booking_*`) ¹ |
186
- | `booking_calendar_create(name, zone?, slotMinutes?, hours?, exceptions?, botId?)` | новый календарь: окна `{day:1–7, from:"HH:mm", to:"HH:mm"}`, исключения `{date, from?, to?}` ¹ |
187
- | `booking_calendar_update(calendarId, …поля)` / `booking_calendar_delete(calendarId, confirm:true)` | правка (поля накладываются на текущие) / удаление ¹ |
188
- | `booking_slots(calendarId, from?, to?, limit?)` | свободные слоты `[{at, label}]`, даты ГГГГ-ММ-ДД в зоне календаря ¹ |
189
- | `booking_list(calendarId, from?)` / `booking_create(calendarId, slotAt, name?, phone?)` / `booking_cancel(calendarId, bookingId)` | брони: список / ручная запись (занятый слот — ошибка «занят») / отмена ¹ |
190
-
191
- ¹ — нужен бэкенд с веткой `feat/battery-completion` (до деплоя вернёт 404).
182
+ | `integration_calls(connectionId?, ok?, limit?)` | журнал вызовов внешних сервисов из сценариев (ошибки, попытки, runId) |
183
+ | `site_lead_status(siteId, leadId, status)` | статус заявки сайта: `NEW`\|`IN_PROGRESS`\|`DONE`\|`REJECTED` (фильтр — `site_leads(status)`) |
184
+ | `bot_rename(botId, name)` / `bot_change_token(botId, token)` | переименовать бота (пустое имя — `@username`) / сменить токен после перевыпуска |
185
+ | `bot_channel_list(botId)` / `bot_channel_add(botId, token, name?)` / `bot_channel_delete(botId, channelId, confirm:true)` | дополнительные Telegram-боты (мультиканальность); каналы/группы для постинга — `list_channels` |
186
+ | `link_create(botId, name?, targetNodeId?)` / `link_delete(linkId, confirm:true)` | стартовая ссылка (диплинк на узел) / удаление; список — `list_links` |
187
+ | `utm_sources(botId)` / `ab_results(graphId, branchNodeId, period?)` | UTM-источники подписчиков / итоги A/B-развилки (`period`: `24h`, `7d`, `30d`) |
188
+ | `kb_delete(kbId, confirm:true)` | удалить базу знаний целиком (база агента — только вместе с агентом) |
189
+ | `agent_unpublish(agentId)` / `agent_delete(agentId, confirm:true)` | снять агента с публикации / удалить (409 `AGENT_IN_USE`, пока подключён к сценариям) |
190
+ | `site_leads_mark_read(siteId)` / `site_lead_delete(siteId, leadId, confirm:true)` / `site_leads_export(siteId, savePath?)` | заявки сайта: всё прочитано / удалить заявку / CSV |
191
+ | `bot_users_export(botId, format?, savePath?)` | выгрузка подписчиков `csv`\|`json` (с `savePath` — в файл) |
192
+ | `bot_user_reset(botId, chatId, confirm:true)` | сбросить сессию подписчика: позиция, метки, переменные (для повторного теста воронки) |
193
+ | `booking_calendar_list()` / `booking_calendar_get(calendarId)` | календари записи (id → `calendarId` действий сценария `booking_*`) |
194
+ | `booking_calendar_create(name, zone?, slotMinutes?, hours?, exceptions?, botId?)` | новый календарь: окна `{day:1–7, from:"HH:mm", to:"HH:mm"}`, исключения `{date, from?, to?}` |
195
+ | `booking_calendar_update(calendarId, …поля)` / `booking_calendar_delete(calendarId, confirm:true)` | правка (поля накладываются на текущие) / удаление |
196
+ | `booking_slots(calendarId, from?, to?, limit?)` | свободные слоты `[{at, label}]`, даты ГГГГ-ММ-ДД в зоне календаря |
197
+ | `booking_list(calendarId, from?)` / `booking_create(calendarId, slotAt, name?, phone?)` / `booking_cancel(calendarId, bookingId)` | брони: список / ручная запись (занятый слот — ошибка «занят») / отмена |
192
198
 
193
199
  ### Рассылки
194
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retensy/mcp",
3
- "version": "0.19.0",
3
+ "version": "0.20.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/validate-graph.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",
25
25
  "validate": "node skills/build-bot-funnel/validate.mjs"
26
26
  },
27
27
  "engines": {
@@ -36,12 +36,15 @@
36
36
  - `TRIGGER_CALLBACK` — `{ "matchMode": "EQUALS"|"STARTS_WITH", "value": "<callback_data>" }`
37
37
  - `TRIGGER_TEXT` — `{ "matchMode": "ANY"|"EQUALS"|"CONTAINS"|"REGEX", "value": "..." }`
38
38
  - `BROADCAST_FILTER` — режим рассылки (если есть — единственный триггер).
39
- - `TRIGGER_PAYMENT` — в кассе владельца прошла оплата.
39
+ - `TRIGGER_PAYMENT` («Оплата прошла») — в кассе владельца прошла оплата: ЮKassa или подключённые через
40
+ `connect_integration` CloudPayments, Robokassa, Т-Банк, Prodamus, bePaid (подтверждённая оплата этих касс
41
+ нормализуется к тому же виду).
40
42
  `{ "minAmount": "1000", "descriptionContains": "курс" }` — оба фильтра необязательны.
41
43
  **Ставь хотя бы один фильтр:** без них сценарий будет запускаться на КАЖДУЮ оплату в кассе.
42
44
  В контексте: `{{payment.id}}`, `{{payment.amount}}`, `{{payment.status}}`,
43
- `{{payment.description}}`, `{{payment.currency}}`, плюс весь ответ ЮKassa как `body`.
44
- Запуск — один раз на платёж: повторные уведомления ЮKassa о той же оплате сценарий не перезапускают. Сюда же приходит оплата счёта, пришедшая уже после `timeout` блока со счётом, — в диалоге того подписчика (пока бот выключен, оплата ждёт повтора ЮKassa).
45
+ `{{payment.description}}`, `{{payment.currency}}`, `{{payment.provider}}` (какая касса), плюс весь документ платежа
46
+ как `body`.
47
+ Запуск — один раз на платёж: повторные уведомления кассы о той же оплате сценарий не перезапускают. Сюда же приходит оплата счёта, пришедшая уже после `timeout` блока со счётом, — в диалоге того подписчика (пока бот выключен, оплата ждёт повтора ЮKassa).
45
48
  - **Счёт выставлял бот** (действие `issue_invoice`) → подписчик определяется САМ: счёт
46
49
  помнит его в `metadata` платежа. Прогон идёт в обычной сессии — можно прямо ставить
47
50
  «Отправить сообщение» и «Добавить метку», указывать получателя и разбирать ответ кассы
@@ -79,7 +82,9 @@
79
82
  (`integration_ingress_url`) ведёт на обработчик конкретного провайдера, и запрос принимается только
80
83
  в формате, который он понимает (подпись/HMAC сервиса проверяется на бэке). Переменные события —
81
84
  `{{body.<ключ>}}`, ключи и примеры — в `inbound.events[].vars` того же события каталога.
82
- `event: "<key>"` с `paymentSucceeded:true` (кассы ЮKassa/CloudPayments/Robokassa/Т-Банк/Prodamus) помимо
85
+ amoCRM шлёт пачку сущностей одним запросом (`leads[add][0..n]`, `leads[status]`, `contacts[add]`) — сценарий
86
+ запускается по КАЖДОЙ сущности отдельно (порядок: сделки добавлены → смена этапа → контакты), не только по первой.
87
+ `event: "<key>"` с `paymentSucceeded:true` (кассы ЮKassa/CloudPayments/Robokassa/Т-Банк/Prodamus/bePaid) помимо
83
88
  этого же `{{body.<ключ>}}`-прогона ЕЩЁ запускает узел «Оплата прошла» ваших графов — headless (без чата)
84
89
  либо в чате подписчика, если платёжную ссылку выдавало действие `*_link`/`issue_invoice` с привязкой
85
90
  к нему. Тестовый/demo-платёж (`test_mode`/`demo_mode` в переменных) прогоняет только обычный вебхук —
@@ -171,10 +176,17 @@ IG-боты не поддерживают команды (`/start`). Вход
171
176
  Ещё есть `subscriber_webhook` — `url` + `method`/`headersJson`/`bodyTemplate`
172
177
  - **уведомления**: `notify` (`text`) — владельцу бота в бот уведомлений из его профиля (Telegram/MAX); не подключён → действие не удалось, блок уходит в `error`; `subscriber_email` (`email`,`text`)
173
178
  - **передача оператору**: `agent_chat` (без полей) — «позвать человека»: диалог встаёт на паузу (бот и ИИ молчат, входящие копятся в «Диалогах»), владельцу уходит уведомление; без бота уведомлений действие помечается `NOT_CONFIGURED`, пауза всё равно включается. Вернуть боту — кнопка в «Диалогах» или MCP `dialog_handoff {active:false}`. В dry-run и headless-прогонах пропускается.
179
+ - **срок ответа оператора (SLA)**: `sla_check` — опц. `maxLevel` (0 — без потолка), `timezone` (для `sla_at`, по умолч. Europe/Moscow). Ставь первым в блоке `ACTIONS` после `DELAY` на срок ответа, ниже в том же блоке — эскалация (`add_tag`, `notify`). Оператор не писал в чат после `agent_chat` → `sla_breached`=`1`, `sla_level` +1, `sla_at` — ISO со смещением, действия блока выполняются; ответил / диалог у бота / тот же таймер повторно → `sla_breached`=`0`, остальные действия блока пропускаются. Новая передача — уровни с 1. Ветвиться дальше — `CONDITION` по `{{var.sla_level}}`.
174
180
  - **запись на слоты** (календарь — `booking_calendar_create`/`booking_calendar_list`): `booking_slots` — `calendarId`, опц. `days` (1–62, по умолч. 7), `limit` (1–50, по умолч. 10), `saveTo` (по умолч. `slots`) → переменные `slots` (нумерованный список для сообщения), `slots_json`, `slots_count`; `booking_book` — `calendarId`, `slotAt` (по умолч. `{{var.slot_at}}`; можно номер из списка «2», ISO-время или подпись слота), опц. `slotsVar`, `name`/`phone` (по умолч. `{{var.name}}`/`{{var.phone}}`) → `booking_id`, `slot_at` (ISO с зоной — для напоминания `DELAY UNTIL {{var.slot_at}} offset -24h`), `slot_label`, `booking_status`; занятый слот → выход блока **`taken`** (если ребро нарисовано, иначе `error`); `booking_cancel` — `bookingId` (по умолч. `{{var.booking_id}}`), освобождает слот.
175
181
  - **контакт заявки**: `lead_link_contact` — `phone`/`email` (по умолч. `{{var.phone}}`/`{{var.email}}`) → склейка с единым контактом владельца по телефону E.164 или email, `{{var.contact_id}}`.
176
182
  - **платный доступ / подписки** (бот должен быть админом группы/канала): `invite_link_create` — персональная ссылка на одно вступление: опц. `expireHours` (по умолч. 24), `name`, `saveTo` (по умолч. `invite_link`), `send` (false — не отправлять), `text`; `invite_link_revoke` — `link` (по умолч. `{{var.invite_link}}`); `subscription_extend` — `period` (`1mo`, `30d`, `1y`, `2w`, `12h`), опц. `var` (по умолч. `sub_until`), `timezone` — продлевает от max(сейчас, текущий срок); `subscription_check` — опц. `var`, `saveTo` (по умолч. `sub_active`) → `true`/`false`; `yookassa_charge_saved` — рекуррентное списание по сохранённому способу оплаты: `connectionId` (ЮKassa), `amount`, `description`, опц. `paymentMethodId` (по умолч. `{{var.yk_payment_method_id}}`), `periodKey` (по умолч. `{{var.sub_until}}` — защита от двойного списания за период). Доступ продлевает не списание, а сценарий `TRIGGER_PAYMENT` → `subscription_extend`.
177
183
  - **Meta Conversions API** (Integration Core): `meta_capi_event` — `connectionId`, `eventName` (Purchase/Lead/…), опц. `dealId`, `stage`, `eventId`, `email`/`phone` (сырые — хешируются SHA-256), `value`, `currency`, `actionSource`. Поля других провайдеров — в `integration_catalog` (`actions[].inputs`).
184
+ - **Integration Core: поля и переменные-результаты** (все — с `connectionId`; пустое/неверное обязательное поле → действие не вызывает сервис, блок уходит в `error`):
185
+ - `ydelivery_price` (Яндекс Доставка) — ВСЕ поля обязательны: `fromLat`, `fromLon`, `toLat`, `toLon`, `fromAddress`, `toAddress` (город, улица, дом — без квартиры/подъезда), `weightKg`, `lengthCm`, `widthCm`, `heightCm` (габариты упаковки в см, > 0, запятая допустима). Без адресов или габаритов — ошибка (заглушек нет). → `ydelivery_price`, `ydelivery_currency`, `ydelivery_eta` (ISO-дедлайн).
186
+ - `cdek_tariff` (СДЭК) — `fromCity`, `toCity`, `weightGrams`, `tariffCode` → `cdek_price` (только доставка), **`cdek_total_sum`** (итог с доп. услугами, 2 знака — его показывай клиенту), `cdek_days_min`, `cdek_days_max`; `cdek_order_status` — `cdekNumber` → `cdek_status`.
187
+ - `moysklad_find_product` — `search` → `moysklad_product_name`, `moysklad_product_price`, **`moysklad_product_id`**; **`moysklad_product_stock`** — остаток: `productId` (обяз., UUID товара/модификации, обычно `{{var.moysklad_product_id}}`), опц. `stockType` (`stock` по умолч., `freeStock`, `quantity`, `reserve`, `inTransit`) → `moysklad_stock` (товар без движений — `0`). Дальше — `CONDITION` по `{{var.moysklad_stock}}`.
188
+ - **`unisender_send_sms`** — одиночное SMS: `phone` (с кодом страны), `sender` (альфа-имя 3–11 символов, зарегистрированное в Unisender), `text` (до 1000 символов) — все обязательны → `unisender_sms_id`. `unisender_subscribe` → `unisender_person_id`.
189
+ - `rest_api_call` — в `body` (JSON) `{{var.x}}` сам ставит кавычки строкам; число или boolean в переменной уходит своим типом (`{"qty": {{var.qty}}}` → `{"qty": 5}`). Тип — по значению переменной, не по виду строки: введённый текст из цифр (телефон) остаётся строкой.
178
190
  - **бот/шаг**: `stop_bot`, `delete_step_message`, `cancel_payment_subscription`
179
191
  - **Google Таблицы (работает)**: `gsheets_send` — дописать строку-заявку в таблицу: `{ "kind":"gsheets_send", "googleEmail":"me@gmail.com", "spreadsheetId":"<id таблицы>", "sheetName":"Лист1", "cells":["{{from.first_name}}","{{var.phone}}","{{var.email}}"] }`. `cells` — значения по порядку (шаблоны), бот дописывает их строкой в конец листа. Google-аккаунт подключается В ВЕБЕ (`/bots` → у действия кнопка «Подключить Google»), НЕ через MCP — у пользователя уже должен быть подключён `googleEmail`. Нужны `googleEmail` + `spreadsheetId` + непустой `cells[]` (иначе `ACTION_GSHEETS_INCOMPLETE`).
180
192
  Остальные четыре действия с таблицами тоже РАБОТАЮТ и тоже требуют `googleEmail` + `spreadsheetId`:
@@ -76,7 +76,7 @@ const ACTION_KINDS = new Set([
76
76
  "group_unban", "group_kick", "group_approve", "group_decline",
77
77
  "invite_link_create", "invite_link_revoke", "subscription_extend", "subscription_check",
78
78
  "yookassa_charge_saved",
79
- "booking_slots", "booking_book", "booking_cancel", "lead_link_contact",
79
+ "booking_slots", "booking_book", "booking_cancel", "lead_link_contact", "sla_check",
80
80
  // Integration Core (провайдеры из integration_catalog) — бэкенд узнаёт их через реестр, здесь — известные:
81
81
  "meta_capi_event",
82
82
  ]);
package/src/index.mjs CHANGED
@@ -27,7 +27,7 @@ import os from "node:os";
27
27
  import fs from "node:fs";
28
28
  import path from "node:path";
29
29
 
30
- const VERSION = "0.19.0";
30
+ const VERSION = "0.20.0";
31
31
  /** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
32
32
  const SITE_INITS = ["starter", "blank", "mini-landing"];
33
33
  /** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
@@ -614,6 +614,16 @@ function qs(params) {
614
614
  return parts.length ? `?${parts.join("&")}` : "";
615
615
  }
616
616
 
617
+ // Выгрузка (CSV/JSON): с savePath — в локальный файл (большие выгрузки не тащим в контекст), без — текстом.
618
+ function exportResult(data, savePath) {
619
+ const text = data == null ? "" : typeof data === "string" ? data : JSON.stringify(data, null, 2);
620
+ if (!savePath) return okResult(text || "Пусто: выгружать нечего.");
621
+ const abs = path.resolve(String(savePath).replace(/^~(?=$|[/\\])/, os.homedir()));
622
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
623
+ fs.writeFileSync(abs, text);
624
+ return okResult({ saved: abs, bytes: Buffer.byteLength(text) });
625
+ }
626
+
617
627
  // =====================================================================// Подключения сервисов
618
628
  // =====================================================================/** IA v2: каталог и подключения — раздел «Интеграции». Старые /bots/connect|integrations остаются в кабинете (Instagram — там). */
619
629
  const CONNECT_PAGE = `${BASE}/integrations`;
@@ -760,14 +770,14 @@ const TOOLS = [
760
770
  { name: "dialog_messages", description: "Переписка с подписчиком, свежие сверху (GET /api/bots/{botId}/users/{chatId}/messages, page/size). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["botId", "chatId"] } },
761
771
  { name: "dialog_reply", description: "ОТПРАВИТЬ сообщение подписчику от имени оператора (POST /api/bots/{botId}/users/{chatId}/messages) — как ответ из раздела «Диалоги». Уходит РЕАЛЬНОМУ человеку: только по явной просьбе пользователя. Бот при этом не останавливается.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, text: { type: "string" } }, required: ["botId", "chatId", "text"] } },
762
772
  // ---- Паритет платформы: журнал вызовов, оператор, заявки, виджет, подписчики, бот ----
763
- { name: "integration_calls", description: "Журнал вызовов внешних сервисов из сценариев (GET /api/bots/integrations/calls): время, подключение, действие, ok/ошибка, код, попытки, correlationId (= runId прогона). Фильтры: connectionId (из list_integrations), ok (true — только успешные, false — только упавшие), limit (1–200). Так проверяют, что CRM/таблица/HTTP реально получили данные. Read-only. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { connectionId: { type: "string" }, ok: { type: "boolean" }, limit: { type: "number" } } } },
764
- { name: "dialog_handoff", description: "Передача диалога оператору (POST /api/bots/{botId}/users/{chatId}/handoff {active}): active:true — бот и ИИ молчат, входящие копятся в «Диалогах», отвечает человек (dialog_reply); active:false — «Вернуть боту». Текущее состояние — поле handoff в bot_user_get. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, active: { type: "boolean" } }, required: ["botId", "chatId", "active"] } },
765
- { name: "site_lead_status", description: "Статус заявки сайта (PATCH /api/bots/pages/{siteId}/leads/{leadId} {status}): NEW → IN_PROGRESS → DONE | REJECTED. leadId — из site_leads. Недопустимый переход → ошибка 409. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, status: { type: "string", enum: ["NEW", "IN_PROGRESS", "DONE", "REJECTED"] } }, required: ["siteId", "leadId", "status"] } },
773
+ { name: "integration_calls", description: "Журнал вызовов внешних сервисов из сценариев (GET /api/bots/integrations/calls): время, подключение, действие, ok/ошибка, код, попытки, correlationId (= runId прогона). Фильтры: connectionId (из list_integrations), ok (true — только успешные, false — только упавшие), limit (1–200). Так проверяют, что CRM/таблица/HTTP реально получили данные. Read-only.", inputSchema: { type: "object", properties: { connectionId: { type: "string" }, ok: { type: "boolean" }, limit: { type: "number" } } } },
774
+ { name: "dialog_handoff", description: "Передача диалога оператору (POST /api/bots/{botId}/users/{chatId}/handoff {active}): active:true — бот и ИИ молчат, входящие копятся в «Диалогах», отвечает человек (dialog_reply); active:false — «Вернуть боту». Текущее состояние — поле handoff в bot_user_get.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, active: { type: "boolean" } }, required: ["botId", "chatId", "active"] } },
775
+ { name: "site_lead_status", description: "Статус заявки сайта (PATCH /api/bots/pages/{siteId}/leads/{leadId} {status}): NEW → IN_PROGRESS → DONE | REJECTED. leadId — из site_leads. Недопустимый переход → ошибка 409.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, status: { type: "string", enum: ["NEW", "IN_PROGRESS", "DONE", "REJECTED"] } }, required: ["siteId", "leadId", "status"] } },
766
776
  { name: "integration_update", description: "Изменить подключение (PUT /api/bots/integrations/{connectionId}): title — новое название; creds — новые ключи доступа (заменяют старые; поля — как в connect_integration для этого провайдера). Передай хотя бы одно.", inputSchema: { type: "object", properties: { connectionId: { type: "string" }, title: { type: "string" }, creds: { type: "object", additionalProperties: { type: "string" } } }, required: ["connectionId"] } },
767
777
  { name: "web_widget_settings", description: "Вид чат-виджета сайта (бот platform WEB). Без settings — прочитать (GET /api/bots/web/{botId}/settings: settings, brandingRemovable, snippet). С settings — изменить (PUT): переданные поля накладываются на текущие, остальные сохраняются; сервер проверяет значения (уходят в разметку чужих сайтов).", inputSchema: { type: "object", properties: { botId: { type: "string" }, settings: { type: "object", description: "Поля настроек виджета для изменения (имена — как в ответе чтения)" } }, required: ["botId"] } },
768
778
  { name: "bot_users_import", description: "Добавить подписчикам метки и поля (POST /api/bots/{botId}/users/import): rows — [{chatId, username?, tags?: [строки], variables?: {ключ: значение}}], до 10 000 строк. Метки ДОБАВЛЯЮТСЯ; переменная пишется, только если у подписчика её ещё нет (существующие не перезаписываются); новый chatId создаёт подписчика (в пределах лимита тарифа). Снять метку или перезаписать поле через API нельзя.", inputSchema: { type: "object", properties: { botId: { type: "string" }, rows: { type: "array", items: { type: "object", properties: { chatId: { type: "string" }, username: { type: "string" }, tags: { type: "array", items: { type: "string" } }, variables: { type: "object" } }, required: ["chatId"] } } }, required: ["botId", "rows"] } },
769
779
  { name: "bot_runs", description: "Журнал прогонов сценариев бота. Без runId — список свежих прогонов (GET /api/bots/{botId}/runs, page/size); с runId — один прогон с шагами (GET /api/bots/runs/{runId}, в т.ч. headless-прогоны вебхук-сценариев и расписаний). По подписчику — bot_user_runs, по сценарию — scenario_runs. Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, runId: { type: "string" }, page: { type: "number" }, size: { type: "number" } } } },
770
- { name: "scenario_runs", description: "Журнал прогонов одного сценария (GET /api/bots/graphs/{graphId}/runs, page/size), включая headless-прогоны без чата — вебхук-сценарии, заявки сайта, TRIGGER_SCHEDULE. Доступ — как к просмотру графа. Шаги прогона — bot_runs {runId}. Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»). Read-only. Требует бэкенд feat/battery-completion.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["graphId"] } },
780
+ { name: "scenario_runs", description: "Журнал прогонов одного сценария (GET /api/bots/graphs/{graphId}/runs, page/size), включая headless-прогоны без чата — вебхук-сценарии, заявки сайта, TRIGGER_SCHEDULE. Доступ — как к просмотру графа. Шаги прогона — bot_runs {runId}. Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»). Read-only.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["graphId"] } },
771
781
  { name: "bot_delete", description: "УДАЛИТЬ бота навсегда (DELETE /api/bots/{botId}) вместе с каналами, сценариями, подписчиками, журналами, рассылками и ссылками. Только владелец. Необратимо: вызывай только по явной просьбе пользователя и с confirm:true. Временно выключить — bot_stop.", inputSchema: { type: "object", properties: { botId: { type: "string" }, confirm: { type: "boolean", description: "true — пользователь явно подтвердил удаление" } }, required: ["botId", "confirm"] } },
772
782
  // ---- Запись на слоты (бронирование) ----
773
783
  { name: "booking_calendar_list", description: "Календари записи пользователя (GET /api/bots/booking/calendars): id, name, zone, slotMinutes, hours, exceptions, botId. id — calendarId для действий сценария booking_slots/booking_book/booking_cancel. Read-only.", inputSchema: { type: "object", properties: {} } },
@@ -780,6 +790,24 @@ const TOOLS = [
780
790
  { name: "booking_create", description: "Записать вручную на слот (POST /api/bots/booking/calendars/{calendarId}/bookings {slotAt, name, phone}): slotAt — at из booking_slots (ISO-8601 с зоной). Слот занят → понятная ошибка «занят», слота нет в сетке → ошибка 400.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, slotAt: { type: "string" }, name: { type: "string" }, phone: { type: "string" } }, required: ["calendarId", "slotAt"] } },
781
791
  { name: "booking_cancel", description: "Отменить бронь и освободить слот (POST /api/bots/booking/calendars/{calendarId}/bookings/{bookingId}/cancel). Повторная отмена — без ошибки. bookingId — из booking_list.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
782
792
  { name: "site_templates", description: "Библиотека шаблонов блоков сайта (GET /api/bots/pages/templates): {categories: [{id, title, description?}], templates: [{id, category, title, description?, blocks: сколько блоков вставится}]}. Вставка — site_edit add_template {container, templateId, after?} (results.id — первый блок, results.ids — все); дальше блоки правятся как обычные. category — фильтр по id категории.", inputSchema: { type: "object", properties: { category: { type: "string" } } } },
793
+ // ---- Паритет платформы, часть 2: бот, база, агент, заявки, подписчики, каналы, ссылки, аналитика ----
794
+ { name: "bot_rename", description: "Переименовать бота (PATCH /api/bots/{botId} {name}): отображаемое имя в кабинете, до 250 символов. Пустое name сбрасывает имя на @username. Имя в самом Telegram/MAX не меняется.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
795
+ { name: "bot_change_token", description: "Сменить токен бота (POST /api/bots/{botId}/token {token}): сервер проверяет токен и перезапускает приём сообщений; сценарии, подписчики и ссылки остаются. Нужен, если токен перевыпущен в @BotFather (/revoke) или MAX. Неверный или чужой токен → ошибка с причиной. Владелец или ADMIN.", inputSchema: { type: "object", properties: { botId: { type: "string" }, token: { type: "string", description: "Новый токен бота" } }, required: ["botId", "token"] } },
796
+ { name: "bot_channel_list", description: "Дополнительные Telegram-боты (мультиканальность) этого бота (GET /api/bots/{botId}/channels): id, username, name, active. Это НЕ каналы/группы для постинга — те в list_channels. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
797
+ { name: "bot_channel_add", description: "Добавить к боту ещё одного Telegram-бота как канал (POST /api/bots/{botId}/channels {token, name?}): он отвечает теми же сценариями. Токен проверяется в Telegram; токен, уже подключённый где-то ещё, не принимается.", inputSchema: { type: "object", properties: { botId: { type: "string" }, token: { type: "string" }, name: { type: "string" } }, required: ["botId", "token"] } },
798
+ { name: "bot_channel_delete", description: "Отключить дополнительного Telegram-бота от бота (DELETE /api/bots/{botId}/channels/{channelId}). channelId — из bot_channel_list. Только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { botId: { type: "string" }, channelId: { type: "string" }, confirm: { type: "boolean" } }, required: ["botId", "channelId", "confirm"] } },
799
+ { name: "link_create", description: "Создать стартовую ссылку бота (POST /api/bots/{botId}/links {name, targetNodeId?}): code для t.me/<бот>?start=<code>, счётчик стартов. targetNodeId — узел сценария, на который ведёт диплинк; для одного узла повторный вызов вернёт существующую ссылку.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" }, targetNodeId: { type: "string" } }, required: ["botId"] } },
800
+ { name: "link_delete", description: "Удалить стартовую ссылку (DELETE /api/bots/links/{linkId}); id — из list_links. Разосланные ссылки перестанут считаться. Только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { linkId: { type: "string" }, confirm: { type: "boolean" } }, required: ["linkId", "confirm"] } },
801
+ { name: "utm_sources", description: "UTM-источники подписчиков бота (GET /api/bots/{botId}/utm-sources): по каким меткам utm_* люди входили в сценарии и сколько их. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
802
+ { name: "ab_results", description: "Результаты A/B-теста (GET /api/bots/graphs/{graphId}/ab-results?branchNodeId&period): статистика по вариантам узла-развилки. branchNodeId — id узла A/B в графе; period — «24h», «7d», «30d» (по умолчанию 7d). Read-only.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, branchNodeId: { type: "string" }, period: { type: "string" } }, required: ["graphId", "branchNodeId"] } },
803
+ { name: "kb_delete", description: "УДАЛИТЬ базу знаний целиком со всеми документами (DELETE /api/bots/kb/{kbId}). База ИИ-агента так не удаляется (ошибка KB_OWNED_BY_AGENT) — удаляй агента. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { kbId: { type: "string" }, confirm: { type: "boolean" } }, required: ["kbId", "confirm"] } },
804
+ { name: "agent_unpublish", description: "Снять ИИ-агента с публикации (POST /api/bots/agents/{agentId}/unpublish): агент возвращается в черновик и перестаёт отвечать. В ответе — agent и сценарии, которые сейчас на нём работают (их стоит проверить).", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
805
+ { name: "agent_delete", description: "УДАЛИТЬ ИИ-агента (DELETE /api/bots/agents/{agentId}). Пока агент или его база подключены к сценариям — ошибка AGENT_IN_USE со списком сценариев: сначала отключи его там. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, confirm: { type: "boolean" } }, required: ["agentId", "confirm"] } },
806
+ { name: "site_leads_mark_read", description: "Отметить все заявки сайта прочитанными (POST /api/bots/pages/{siteId}/leads/read): обнуляет счётчик unread из site_leads. Статусы обработки не меняются (это site_lead_status).", inputSchema: { type: "object", properties: { siteId: { type: "string" } }, required: ["siteId"] } },
807
+ { name: "site_lead_delete", description: "УДАЛИТЬ заявку сайта (DELETE /api/bots/pages/{siteId}/leads/{leadId}); leadId — из site_leads. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, confirm: { type: "boolean" } }, required: ["siteId", "leadId", "confirm"] } },
808
+ { name: "site_leads_export", description: "Все заявки сайта в CSV (GET /api/bots/pages/{siteId}/leads.csv). savePath — сохранить в локальный файл и вернуть путь (для больших выгрузок); без него CSV возвращается текстом. Read-only.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, savePath: { type: "string" } }, required: ["siteId"] } },
809
+ { name: "bot_users_export", description: "Выгрузка всех подписчиков бота (GET /api/bots/{botId}/users/export?format=csv|json): chatId, имя, метки, переменные. savePath — сохранить в локальный файл и вернуть путь; без него данные возвращаются текстом. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, format: { type: "string", enum: ["csv", "json"] }, savePath: { type: "string" } }, required: ["botId"] } },
810
+ { name: "bot_user_reset", description: "Сбросить сессию подписчика (DELETE /api/bots/{botId}/sessions/{chatId}): стираются его позиция в сценарии, метки и переменные, и он пропадает из list_bot_users; при следующем сообщении начнёт как новый. Для повторного прохождения воронки при тесте. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, confirm: { type: "boolean" } }, required: ["botId", "chatId", "confirm"] } },
783
811
  ];
784
812
 
785
813
  async function handleCall(params) {
@@ -1254,6 +1282,114 @@ async function handleCall(params) {
1254
1282
  await api(`/api/bots/${botId}`, { method: "DELETE" });
1255
1283
  return okResult(`🗑️ Бот ${botId} удалён вместе со сценариями и подписчиками.`);
1256
1284
  }
1285
+ // ---- Паритет платформы, часть 2 ----
1286
+ case "bot_rename": {
1287
+ if (typeof a.name !== "string") throw new Error("name — строка (пустая сбрасывает имя на @username).");
1288
+ return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}`, { method: "PATCH", body: { name: a.name } }));
1289
+ }
1290
+ case "bot_change_token": {
1291
+ const botId = uuidArg(a.botId, "botId");
1292
+ const token = String(a.token ?? "").trim();
1293
+ if (!token) throw new Error("Передай token — новый токен бота.");
1294
+ try {
1295
+ return okResult(await api(`/api/bots/${botId}/token`, { method: "POST", body: { token } }));
1296
+ } catch (e) {
1297
+ // 400 — строка-причина (неверный токен, уже подключён к другому боту); сам токен в ошибку не кладём.
1298
+ if (e.status === 400) throw new Error(`Токен не принят: ${typeof e.data === "string" && e.data ? e.data : "проверь токен"}.`);
1299
+ throw e;
1300
+ }
1301
+ }
1302
+ case "bot_channel_list": return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/channels`));
1303
+ case "bot_channel_add": {
1304
+ const botId = uuidArg(a.botId, "botId");
1305
+ const token = String(a.token ?? "").trim();
1306
+ if (!token) throw new Error("Передай token — токен дополнительного Telegram-бота.");
1307
+ try {
1308
+ return okResult(await api(`/api/bots/${botId}/channels`, { method: "POST", body: { token, name: a.name || undefined } }));
1309
+ } catch (e) {
1310
+ if (e.status === 400) throw new Error(`Канал не добавлен: ${typeof e.data === "string" && e.data ? e.data : "токен не принят"}.`);
1311
+ throw e;
1312
+ }
1313
+ }
1314
+ case "bot_channel_delete": {
1315
+ const botId = uuidArg(a.botId, "botId");
1316
+ const channelId = uuidArg(a.channelId, "channelId");
1317
+ if (a.confirm !== true) throw new Error("Отключение канала необратимо. Спроси пользователя и повтори с confirm:true.");
1318
+ await api(`/api/bots/${botId}/channels/${channelId}`, { method: "DELETE" });
1319
+ return okResult(`🗑️ Канал ${channelId} отключён от бота ${botId}.`);
1320
+ }
1321
+ case "link_create": {
1322
+ const body = { name: a.name || undefined };
1323
+ if (a.targetNodeId) body.targetNodeId = uuidArg(a.targetNodeId, "targetNodeId");
1324
+ return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/links`, { method: "POST", body }));
1325
+ }
1326
+ case "link_delete": {
1327
+ const linkId = uuidArg(a.linkId, "linkId");
1328
+ if (a.confirm !== true) throw new Error("Удаление ссылки необратимо. Спроси пользователя и повтори с confirm:true.");
1329
+ await api(`/api/bots/links/${linkId}`, { method: "DELETE" });
1330
+ return okResult(`🗑️ Ссылка ${linkId} удалена.`);
1331
+ }
1332
+ case "utm_sources": return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/utm-sources`));
1333
+ case "ab_results": {
1334
+ const q = { branchNodeId: uuidArg(a.branchNodeId, "branchNodeId") };
1335
+ if (a.period != null && a.period !== "") {
1336
+ const p = String(a.period).trim();
1337
+ if (!/^\d{1,4}[hd]$/.test(p)) throw new Error(`period — число с h или d (24h, 7d, 30d), получено: ${p}.`);
1338
+ q.period = p;
1339
+ }
1340
+ return okResult(await api(`/api/bots/graphs/${uuidArg(a.graphId, "graphId")}/ab-results${qs(q)}`));
1341
+ }
1342
+ case "kb_delete": {
1343
+ const kbId = idArg(a.kbId, "kbId");
1344
+ if (a.confirm !== true) throw new Error("Удаление базы знаний необратимо. Спроси пользователя и повтори с confirm:true.");
1345
+ try {
1346
+ await api(`/api/bots/kb/${kbId}`, { method: "DELETE" });
1347
+ } catch (e) {
1348
+ if (e.status === 409) throw new Error("Это база ИИ-агента (KB_OWNED_BY_AGENT): отдельно не удаляется — удали агента (agent_delete).");
1349
+ throw e;
1350
+ }
1351
+ return okResult(`🗑️ База знаний ${kbId} удалена со всеми документами.`);
1352
+ }
1353
+ case "agent_unpublish": return okResult(await api(`/api/bots/agents/${idArg(a.agentId, "agentId")}/unpublish`, { method: "POST" }));
1354
+ case "agent_delete": {
1355
+ const agentId = idArg(a.agentId, "agentId");
1356
+ if (a.confirm !== true) throw new Error("Удаление агента необратимо. Спроси пользователя и повтори с confirm:true.");
1357
+ try {
1358
+ await api(`/api/bots/agents/${agentId}`, { method: "DELETE" });
1359
+ } catch (e) {
1360
+ if (e.status === 409) {
1361
+ const refs = Array.isArray(e.data?.scenarios) ? e.data.scenarios.map((s) => s?.graphName || s?.graphId).filter(Boolean) : [];
1362
+ throw new Error(`Агент подключён к сценариям (AGENT_IN_USE)${refs.length ? `: ${refs.join(", ")}` : ""}. Сначала отключи его в них.`);
1363
+ }
1364
+ throw e;
1365
+ }
1366
+ return okResult(`🗑️ Агент ${agentId} удалён.`);
1367
+ }
1368
+ case "site_leads_mark_read": {
1369
+ const siteId = uuidArg(a.siteId, "siteId");
1370
+ await api(`/api/bots/pages/${siteId}/leads/read`, { method: "POST" });
1371
+ return okResult(`✅ Все заявки сайта ${siteId} отмечены прочитанными.`);
1372
+ }
1373
+ case "site_lead_delete": {
1374
+ const siteId = uuidArg(a.siteId, "siteId");
1375
+ const leadId = uuidArg(a.leadId, "leadId");
1376
+ if (a.confirm !== true) throw new Error("Удаление заявки необратимо. Спроси пользователя и повтори с confirm:true.");
1377
+ await api(`/api/bots/pages/${siteId}/leads/${leadId}`, { method: "DELETE" });
1378
+ return okResult(`🗑️ Заявка ${leadId} удалена.`);
1379
+ }
1380
+ case "site_leads_export": return exportResult(await api(`/api/bots/pages/${uuidArg(a.siteId, "siteId")}/leads.csv`), a.savePath);
1381
+ case "bot_users_export": {
1382
+ const format = String(a.format || "csv").trim().toLowerCase();
1383
+ if (format !== "csv" && format !== "json") throw new Error(`format: csv | json, получено: ${format}.`);
1384
+ return exportResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/users/export?format=${format}`), a.savePath);
1385
+ }
1386
+ case "bot_user_reset": {
1387
+ const botId = uuidArg(a.botId, "botId");
1388
+ const chatId = chatIdArg(a.chatId);
1389
+ if (a.confirm !== true) throw new Error("Сброс сессии стирает метки и переменные подписчика. Спроси пользователя и повтори с confirm:true.");
1390
+ await api(`/api/bots/${botId}/sessions/${chatId}`, { method: "DELETE" });
1391
+ return okResult(`♻️ Сессия подписчика ${chatId} сброшена: при следующем сообщении он начнёт как новый.`);
1392
+ }
1257
1393
  // ---- Боты ----
1258
1394
  case "create_bot": {
1259
1395
  const platform = String(a.platform || "").trim().toUpperCase();