@retensy/mcp 0.39.0 → 0.40.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
|
@@ -238,6 +238,10 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
238
238
|
| `broadcast_preview(botIds, tagsAll?, tagsNone?, audienceFilter?, segmentId?)` | оценка охвата: сколько подписчиков получат рассылку |
|
|
239
239
|
| `broadcast_send(name, botIds, messages \| graphId, tagsAll?/tagsNone? или audienceFilter?, scheduledAt?, draftId?, segmentId?, localTime?, dedup?, stagger?)` | отправить сейчас / запланировать; по нескольким ботам; или запуск сценария; `dedup`/`stagger` → мульти-бот группа (`broadcast_group`) вместо прямых рассылок |
|
|
240
240
|
| `audience_segments(action, botId, …)` | сохранённые сегменты аудитории: list / create {name, audienceFilter} / delete {segmentId}; дальше `segmentId` в `broadcast_send`/`broadcast_preview` |
|
|
241
|
+
| `bot_access(botId, action, …)` | закрытый бот и список доступа: get / settings {privateBot, deniedMessage} / add {items[{kind TG_ID\|USERNAME\|PHONE, value, note?}] или text} / remove {entryId} / clear {confirm:true}; проверка в сценарии - узел `ACCESS_GATE` |
|
|
242
|
+
| `preset_list(botId?)` / `preset_save(name, nodeType, config, presetId?, botId?)` / `preset_delete(presetId)` | пресеты блоков (аккаунт или бот). Секретные заголовки хранятся зашифрованно и приходят ссылками `preset-secret:<id>:<Header>` - их подставляют в `headersJson` узла, открытым текстом секреты не выводятся |
|
|
243
|
+
| `webhook_requests(graphId, action?)` | входящие запросы сценария-вебхука (последние 50 за 7 дней, секреты скрыты): list / listen (тестовый URL `/api/webhooks/t/<path>`) / stop / clear {confirm:true}; для `TRIGGER_WEBHOOK` `mappings` |
|
|
244
|
+
| `http_test(url, method?, headersJson?, bodyTemplate?, timeoutMs?, vars?)` | тестовый запрос с сервера (SSRF-защита, 15 с, 1 МБ): status, headers, body и `fields` (JsonPath) для `extract:[{path,saveTo}]` |
|
|
241
245
|
| `bot_delivery_window(action, botId, …)` | тихие часы бота (по умолчанию выкл): get / set {enabled, quietFrom, quietTo, timezone?}; рассылки в окне переносятся на его конец, пояс - подписчика (переменная `timezone`), иначе бота |
|
|
242
246
|
| `broadcast_test(botId, messages, chatId?)` | тест рассылки себе: на привязанный Telegram владельца или в чат подписчика; мимо квоты и очереди |
|
|
243
247
|
| `broadcast_cancel(broadcastId)` | отменить запланированную/идущую (для группы — `broadcast_group action:"cancel"`) |
|
|
@@ -257,6 +261,8 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
257
261
|
| `agent_health(agentId)` | счётчики документов/фрагментов базы знаний |
|
|
258
262
|
| `agent_test_chat(agentId, question, history?)` | проверить ответ в песочнице (**тратит бюджет ИИ** — не вызывай массово) |
|
|
259
263
|
| `agent_unanswered(agentId, days?)` | вопросы без ответа за период (7\|30\|90, по умолчанию 30) |
|
|
264
|
+
| `analytics_ask(question)` | вопрос к аналитике по-русски: бэкенд переводит его в вызов белого списка запросов по данным владельца, ответ с числом, таблицей и «как посчитано»; тратит квоту ИИ |
|
|
265
|
+
| `faq_suggestions(agentId, action, suggestionId?, question?, answer?)` | авто-FAQ: list / generate / approve (в базу знаний как QA) / dismiss |
|
|
260
266
|
| `agent_tests_run(agentId, confirm, caseIds?, generate?, wait?, timeoutSec?)` | проверка агента ИИ-клиентами по тест-кейсам: диалог в песочнице, ИИ-судья (PASS/FAIL, причина, цитата), сравнение с прошлым прогоном. **Тратит бюджет ИИ** — нужен `confirm:true` |
|
|
261
267
|
| `agent_tests_list(agentId, runId?)` | тест-кейсы, история прогонов и полный прогон (последний или по `runId`) с регрессиями; ничего не тратит |
|
|
262
268
|
| `kb_docs(kbId)` | документы базы знаний агента (источник, статус, фрагменты) |
|
|
@@ -283,6 +289,8 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
283
289
|
| `link_create(botId, name?, targetNodeId?)` / `link_delete(linkId, confirm:true)` | стартовая ссылка (диплинк на узел) / удаление; список — `list_links` |
|
|
284
290
|
| `utm_sources(botId)` / `ab_results(graphId, branchNodeId, period?)` | UTM-источники подписчиков / итоги A/B-развилки (`period`: `24h`, `7d`, `30d`) |
|
|
285
291
|
| `source_revenue(botId, from, to)` | Отчёт источник -> подписчики/покупатели/выручка (RUB + Stars)/конверсия, даты YYYY-MM-DD |
|
|
292
|
+
| `bot_health(botId?)` | Здоровье бота: статус OK/WARN/CRIT, проблемы с подсказками (токен, 409, опрос, ошибки сценариев, платежи, конверсия, ИИ-агент); без botId - все боты |
|
|
293
|
+
| `unsubscribe_reasons(botId, days?)` | Причины отписки: блокировки, кнопки, темы свободных ответов (ИИ раз в неделю) |
|
|
286
294
|
| `kb_delete(kbId, confirm:true)` | удалить базу знаний целиком (база агента — только вместе с агентом) |
|
|
287
295
|
| `agent_unpublish(agentId)` / `agent_delete(agentId, confirm:true)` | снять агента с публикации / удалить (409 `AGENT_IN_USE`, пока подключён к сценариям) |
|
|
288
296
|
| `site_leads_mark_read(siteId)` / `site_lead_delete(siteId, leadId, confirm:true)` / `site_leads_export(siteId, savePath?)` | заявки сайта: всё прочитано / удалить заявку / CSV |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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": {
|
|
@@ -75,7 +75,8 @@
|
|
|
75
75
|
Бот должен быть админом канала/группы, иначе события оттуда не придут.
|
|
76
76
|
- `TRIGGER_ANY_UPDATE` — `{}`, ловит ЛЮБОЙ апдейт. Приоритет самый низкий: срабатывает, только
|
|
77
77
|
если не подошёл ни один конкретный триггер. Удобен как «ничего не понял» / отладка.
|
|
78
|
-
- `
|
|
78
|
+
- `ACCESS_GATE` — проверка доступа, выходы `allowed` / `denied`. `{ "match":"ANY"|"ALL", "rules":[{ "kind": ALLOWLIST|TG_ID|USERNAME|PHONE|TAG|SEGMENT|PAID_SUBSCRIPTION|VARIABLE, "value"?, "op"?, "key"? }] }`. `ALLOWLIST` смотрит список доступа бота (`bot_access`); `TG_ID`/`USERNAME`/`PHONE` требуют `value`; `TAG` — `value` = тег; `SEGMENT` — `value` = id сегмента (`audience_segments`); `VARIABLE` — `key` (+`op`, `value`, как в CONDITION); `PAID_SUBSCRIPTION` — активная платная подписка. Не для Instagram.
|
|
79
|
+
- `TRIGGER_WEBHOOK` — `{}` (необязательно `mappings:[{path,saveTo}]`: JsonPath из тела входящего запроса `$.data.email` → переменная подписчика; реальное тело смотри `webhook_requests`), точка входа сценария с источником WEBHOOK (не бот); у графа БОТ-сценария
|
|
79
80
|
этот триггер не сработает. Такой сценарий создаётся `scenario_create(name)` без `botId` — сервер сам
|
|
80
81
|
выпускает путь входа и секрет (видны в `scenario_get` → `source.config`, ротируются `scenario_rotate_secret`);
|
|
81
82
|
узлы/рёбра — как у обычного графа, через `update_graph`/`edit_graph_live`/`patch_graph` по вернувшемуся id.
|
|
@@ -98,6 +99,11 @@
|
|
|
98
99
|
либо в чате подписчика, если платёжную ссылку выдавало действие `*_link`/`issue_invoice` с привязкой
|
|
99
100
|
к нему. Тестовый/demo-платёж (`test_mode`/`demo_mode` в переменных) прогоняет только обычный вебхук —
|
|
100
101
|
«Оплата прошла» на нём не срабатывает.
|
|
102
|
+
- `TRIGGER_BOOKING` — события записи: `{ "events": ["booked","confirmed","cancelled","reminder","after_visit"], "calendarId": "<uuid, необязательно = любой>", "hoursBefore": 24, "hoursAfter": 2 }`.
|
|
103
|
+
`events` непустой (`BOOKING_TRIGGER_NO_EVENTS`/`BOOKING_TRIGGER_BAD_EVENT`), `calendarId` — UUID (`BOOKING_TRIGGER_BAD_CALENDAR`),
|
|
104
|
+
`hoursBefore` 1..720 для `reminder`, `hoursAfter` 1..168 для `after_visit` (`BOOKING_TRIGGER_BAD_HOURS`). Если запись сделана через бота,
|
|
105
|
+
сценарий идёт в диалоге клиента; иначе без получателя. Переменные: `{{booking.id}}`, `{{booking.event}}`, `{{booking.calendar}}`,
|
|
106
|
+
`{{booking.start}}`, `{{booking.end}}`, `{{booking.label}}`, `{{booking.name}}`, `{{booking.phone}}`. Каждое событие на запись срабатывает один раз.
|
|
101
107
|
- `TRIGGER_SITE_FORM` — `{ "siteId": "<uuid сайта>", "formId": "<id формы>" }` — заявка с формы
|
|
102
108
|
опубликованного сайта владельца. `formId` пустой/нет = любая форма сайта. `siteId` обязателен
|
|
103
109
|
(`SITE_FORM_NO_SITE`), сайт должен быть свой (`SITE_FORM_FOREIGN_SITE`). Переменные: поля формы
|
|
@@ -220,6 +226,7 @@ IG-боты не поддерживают команды (`/start`). Вход
|
|
|
220
226
|
- **передача оператору**: `agent_chat` (без полей) — «позвать человека»: диалог встаёт на паузу (бот и ИИ молчат, входящие копятся в «Диалогах»), владельцу уходит уведомление; без бота уведомлений действие помечается `NOT_CONFIGURED`, пауза всё равно включается. Вернуть боту — кнопка в «Диалогах» или MCP `dialog_handoff {active:false}`. В dry-run и headless-прогонах пропускается.
|
|
221
227
|
- **срок ответа оператора (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}}`.
|
|
222
228
|
- **запись на слоты** (календарь — `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}}`), освобождает слот.
|
|
229
|
+
- **публикации из сценария**: `publish_post` — `target` `"channel"` (+ `chatId` привязанного канала TG/MAX, `bot_channel_list`) или `"vk"` (+ `accountId` из `vk_status`), `text` и/или `mediaUrl` (наш `/api/bots/media`), шаблоны `{{var.*}}`; результат — `{{var.post_id}}`; публикует от имени владельца, расходует месячный лимит, в «Тесте» пропускается. `chain_enqueue` — `chainId` (включённая цепочка владельца, `chain_list`), `text`/`mediaUrl`: содержимое входит в цепочку как новый пост источника (фильтры, очередь, назначения), результат — `{{var.chain_jobs}}`. `start_broadcast` — запуск рассылки из сценария: `draftId` (UUID черновика, `broadcast_drafts`), необязательный `segmentId` (сохранённый сегмент, `audience_segments`; пусто = аудитория черновика), `minIntervalHours` 1..720 (по умолчанию 24: не чаще одного запуска за срок на связку бот+черновик+сегмент), `idempotencyKey` (шаблон до 100 символов: повтор с тем же ключом не запускает вторую рассылку). Защиты: лимит запусков на владельца в час, тариф и пробная квота рассылок, тихие часы и дедупликация движка рассылок, запись в журнал аудита; в «Тесте» пропускается. Результат: `{{var.broadcast_id}}`, `{{var.broadcast_status}}` (STARTED|DUPLICATE|THROTTLED|RATE_LIMITED|NOT_ALLOWED|NO_AUDIENCE). Коды: `BROADCAST_BAD_DRAFT`, `BROADCAST_BAD_SEGMENT`, `BROADCAST_BAD_INTERVAL`, `PUBLISH_BAD_TARGET`, `PUBLISH_NO_DESTINATION`, `PUBLISH_EMPTY`, `CHAIN_ENQUEUE_BAD_CHAIN`, `CHAIN_ENQUEUE_EMPTY`.
|
|
223
230
|
- **переход в другой сценарий**: `call_scenario` — `graphId` (сценарий ЭТОГО ЖЕ бота того же владельца, id из `list_graphs`; черновик тоже можно), опц. `nodeId` (стартовый блок цели; по умолч. триггер `/start` → первый триггер → первый блок без входящих рёбер — так стартует подсценарий без триггера), `variables: [{ "key":"plan", "value":"{{var.x}}" }]` (ключи `[a-z][a-z0-9_]{0,63}`, служебные `_*` запрещены — `ACTION_BAD_KEY`, в рантайме пропускаются, значения — шаблоны; сохраняются подписчику ДО перехода), `mode` — `"goto"` (по умолч.) или `"call"`. Ставь ПОСЛЕДНИМ в блоке для обоих режимов (`CALL_SCENARIO_NOT_LAST`), не на себя (`CALL_SCENARIO_SELF`), только `goto`/`call` (иначе `CALL_SCENARIO_BAD_MODE`), без цели — `CALL_SCENARIO_NO_TARGET`, чужой/несуществующий/вебхук-сценарий — `CALL_SCENARIO_FORBIDDEN` при публикации.
|
|
224
231
|
- `mode:"goto"` (по умолчанию) — подписчик уходит в целевой сценарий отдельным прогоном, текущий прогон завершается `OK`; ответы на вопросы и кнопки цели доходят до неё, хотя активный сценарий бота другой (`session.currentGraphId`). Возврата нет — для «вернуться» нужно либо `mode:"call"`, либо свой `call_scenario` обратно в конце цели (сработает, только если между ними был ответ подписчика/задержка, иначе `CALL_SCENARIO_RECURSION`).
|
|
225
232
|
- `mode:"call"` — «вызвать и вернуться»: перед переходом движок кладёт возвратный кадр (вызывающий graphId + узел, на который вести после — следующий по ребру `next` этого же блока, либо ничего, если ребра нет) в `session.callStack` (персистентно, переживает рестарт сервера). Когда цель доходит до `END` или до конца графа (рёбер больше нет), движок снимает кадр и продолжает вызывающего С ЭТОГО МЕСТА новым прогоном — удобно для переиспользуемых подсценариев («спросить телефон», «проверить оплату») без копипасты.
|
|
@@ -529,8 +536,9 @@ node validate.mjs graph.json --platform=INSTAGRAM
|
|
|
529
536
|
| `BRANCH` | `case_<id>`, `default` |
|
|
530
537
|
| `ASK_QUESTION` | `valid`, `invalid` |
|
|
531
538
|
| `SEND_MESSAGE` с `awaitReply:true` | `valid`, `invalid` (+ `btn_N` для кнопок) |
|
|
532
|
-
| `CALL_WEBHOOK` | `ok`, `error` |
|
|
533
|
-
| `
|
|
539
|
+
| `CALL_WEBHOOK` | `ok`, `error`, необязательный `timeout` (запрос не уложился в `timeoutMs`; без ребра идёт в `error`) |
|
|
540
|
+
| `ACCESS_GATE` | `allowed`, `denied` |
|
|
541
|
+
| `ACTIONS` | `next`, плюс `timeout` (необязательный: `external_request` не уложился в таймаут) и `error` — если внутри есть действие, которое может упасть (внешний запрос, CRM, Таблицы); `taken` — слот `booking_book` занят |
|
|
534
542
|
| `SWITCH` | `case_<id>`, `default` |
|
|
535
543
|
| `RANDOM_TEXT` | `next`, `error` |
|
|
536
544
|
| `STOP_AND_ERROR` | выходов нет (терминатор) |
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
- `ASK_QUESTION`: `promptText` обязателен (`ASK_NO_PROMPT`); `saveTo` ∈ `[a-z_][a-z0-9_]{0,63}` (`ASK_BAD_SAVE_TO`); при `validator:"REGEX"` — валидный `regex` (`ASK_BAD_REGEX`).
|
|
47
47
|
- `BRANCH`: ≥1 case (`BRANCH_NO_CASES`); вне `abTest` — непустое валидное `expression` (`BRANCH_EMPTY_EXPR`/`BRANCH_BAD_EXPR`) и подключённое ребро `case_<id>` (`BRANCH_CASE_UNCONNECTED`; `disabled:true` — если ветка намеренно пустая). В `abTest:true` рёбра нужны только у первых двух case.
|
|
48
48
|
- `CONDITION`: `match` ∈ {ALL, ANY} (`CONDITION_BAD_MATCH`); ≥1 элемент в `conditions` (`CONDITION_EMPTY`). Для `kind:"TAG"` — `value` ∈ `[a-z0-9_-]{1,64}` (`CONDITION_BAD_TAG`); для `kind:"VARIABLE"` — `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`CONDITION_BAD_KEY`). Остальные `kind` (UTM/NAME/EMAIL/PHONE/USERNAME/SUBSCRIBED/LINK_CLICKED/BUSINESS/CURRENT_*/DAY_OF_WEEK) бэкенд проверяет в рантайме — некорректный `kind`/`op`, нечисловой `key` у `SUBSCRIBED` или `key` у `LINK_CLICKED` без отслеживаемой кнопки молча дают `false` (выход `no`), публикацию не блокируют. Список `kind`/`op` — в schema.md.
|
|
49
|
+
- `ACCESS_GATE`: `match` ∈ {ANY,ALL} (`ACCESS_GATE_BAD_MATCH`); ≥1 правило (`ACCESS_GATE_NO_RULES`); `kind` ∈ {ALLOWLIST,TG_ID,USERNAME,PHONE,TAG,SEGMENT,PAID_SUBSCRIPTION,VARIABLE} (`ACCESS_GATE_BAD_KIND`); `TG_ID`/`USERNAME`/`PHONE` без `value` — `ACCESS_GATE_NO_VALUE`; `SEGMENT` без `value` — `ACCESS_GATE_NO_SEGMENT`; `VARIABLE` без `key` — `ACCESS_GATE_NO_KEY`.
|
|
50
|
+
- `TRIGGER_WEBHOOK`: `mappings` — массив `{path,saveTo}`; `path` начинается с `$` (`WEBHOOK_BAD_MAPPING_PATH`), `saveTo` ∈ `[a-z_][a-z0-9_]{0,63}` (`WEBHOOK_BAD_MAPPING_SAVE_TO`).
|
|
49
51
|
- `CALL_WEBHOOK`: `url` обязателен (`WEBHOOK_NO_URL`), http(s):// (`WEBHOOK_BAD_SCHEME`).
|
|
50
52
|
- `DELAY`: `kind` ∈ {FIXED,UNTIL,TOMORROW} (`DELAY_BAD_KIND`). `FIXED` — `durationSec>0` (секунды) ИЛИ `duration>0` (+`unit` ∈ {MINUTES,HOURS,DAYS}), иначе `DELAY_BAD_DURATION`. `TOMORROW` — `time` = `HH:mm` (`DELAY_BAD_TIME`). `UNTIL` — `isoTimestamp`: ISO-8601 или шаблон `{{var.x}}` (`DELAY_BAD_TIMESTAMP`; рантайм читает только его, не `isoDate`+`time`); опц. `offset` вида `-24h`/`-30m`/`+1d` (`DELAY_BAD_OFFSET`) и `timezone` (`DELAY_BAD_TIMEZONE`).
|
|
51
53
|
- `SET_VARIABLE`: `key` ∈ `[a-z_][a-z0-9_]{0,63}` (`VAR_BAD_KEY`). `ADD_TAG`/`REMOVE_TAG`: `tag` ∈ `[a-z0-9_-]{1,64}` (`TAG_BAD_NAME`).
|
|
@@ -85,6 +87,10 @@
|
|
|
85
87
|
- `YOOKASSA_PAYMENT`: **устаревший узел, в новых сценариях не ставить** — счёт собирается действием
|
|
86
88
|
`issue_invoice` внутри `ACTIONS`. Если узел всё же есть: `connectionId` (`YK_NO_CONNECTION`),
|
|
87
89
|
`amount` (`YK_NO_AMOUNT`/`YK_BAD_AMOUNT`), `description` (`YK_NO_DESC`).
|
|
90
|
+
- `TRIGGER_BOOKING`: `events` непустой список из booked|confirmed|cancelled|reminder|after_visit (`BOOKING_TRIGGER_NO_EVENTS`, `BOOKING_TRIGGER_BAD_EVENT`);
|
|
91
|
+
`calendarId` UUID или пусто (`BOOKING_TRIGGER_BAD_CALENDAR`); `hoursBefore` 1..720, `hoursAfter` 1..168 (`BOOKING_TRIGGER_BAD_HOURS`).
|
|
92
|
+
- `start_broadcast` (в `ACTIONS`): `draftId` UUID (`BROADCAST_BAD_DRAFT`), `segmentId` UUID или пусто (`BROADCAST_BAD_SEGMENT`),
|
|
93
|
+
`minIntervalHours` целое 1..720 (`BROADCAST_BAD_INTERVAL`).
|
|
88
94
|
- `TRIGGER_PAYMENT`: обязательных полей нет. Фильтры `minAmount`/`descriptionContains`
|
|
89
95
|
необязательны, но **без них сценарий стартует на КАЖДУЮ оплату в кассе**, включая продажи
|
|
90
96
|
мимо бота. Валидатор это не блокирует (бэкенд тоже) — следи сам.
|
|
@@ -79,6 +79,7 @@ const ACTION_KINDS = new Set([
|
|
|
79
79
|
"yookassa_charge_saved",
|
|
80
80
|
"booking_slots", "booking_book", "booking_cancel", "lead_link_contact", "sla_check",
|
|
81
81
|
"call_scenario",
|
|
82
|
+
"publish_post", "chain_enqueue", "start_broadcast",
|
|
82
83
|
"send_email",
|
|
83
84
|
// Integration Core (провайдеры из integration_catalog) — бэкенд узнаёт их через реестр, здесь — известные:
|
|
84
85
|
"meta_capi_event",
|
|
@@ -245,6 +246,20 @@ for (const n of nodes) {
|
|
|
245
246
|
else if (!UUID_RE.test(String(c.siteId).trim())) errors.push(`SITE_FORM_NO_SITE: ${who} — siteId «${c.siteId}» не UUID сайта (site_list).`);
|
|
246
247
|
if (c.formId != null && typeof c.formId !== "string") errors.push(`${who}: formId — строка (id формы из site_get) или пусто = любая форма.`);
|
|
247
248
|
break;
|
|
249
|
+
case "TRIGGER_BOOKING": {
|
|
250
|
+
const EVENTS = ["booked", "confirmed", "cancelled", "reminder", "after_visit"];
|
|
251
|
+
if (!Array.isArray(c.events) || c.events.length === 0) {
|
|
252
|
+
errors.push(`BOOKING_TRIGGER_NO_EVENTS: ${who} — events: непустой список из ${EVENTS.join("|")}.`);
|
|
253
|
+
} else {
|
|
254
|
+
for (const e of c.events) if (!EVENTS.includes(e)) errors.push(`BOOKING_TRIGGER_BAD_EVENT: ${who} — неизвестное событие «${e}» (${EVENTS.join("|")}).`);
|
|
255
|
+
}
|
|
256
|
+
if (!blank(c.calendarId) && !UUID_RE.test(String(c.calendarId).trim()))
|
|
257
|
+
errors.push(`BOOKING_TRIGGER_BAD_CALENDAR: ${who} — calendarId = UUID календаря (booking_calendar_list) или пусто = любой.`);
|
|
258
|
+
const hrs = (k, max) => c[k] != null && !(Number.isInteger(c[k]) && c[k] >= 1 && c[k] <= max);
|
|
259
|
+
if (hrs("hoursBefore", 720)) errors.push(`BOOKING_TRIGGER_BAD_HOURS: ${who} — hoursBefore — целое 1..720 (по умолчанию 24).`);
|
|
260
|
+
if (hrs("hoursAfter", 168)) errors.push(`BOOKING_TRIGGER_BAD_HOURS: ${who} — hoursAfter — целое 1..168 (по умолчанию 2).`);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
248
263
|
case "TRIGGER_COMMENT":
|
|
249
264
|
errors.push(`TRIGGER_COMMENT_DEAD: ${who} — этот тип триггера не сработает никогда (рантайм не выставляет event=comment). Для Instagram используй TRIGGER_IG_COMMENT.`);
|
|
250
265
|
break;
|
|
@@ -252,6 +267,30 @@ for (const n of nodes) {
|
|
|
252
267
|
if (blank(c.promptText)) errors.push(`${who}: нужен promptText.`);
|
|
253
268
|
if (c.saveTo && !VAR_RE.test(c.saveTo)) errors.push(`${who}: saveTo «${c.saveTo}» не матчит [a-z_][a-z0-9_]{0,63}.`);
|
|
254
269
|
break;
|
|
270
|
+
case "TRIGGER_WEBHOOK": {
|
|
271
|
+
// config.mappings [{path,saveTo}]: JsonPath из тела входящего запроса -> переменная подписчика.
|
|
272
|
+
if (c.mappings != null && !Array.isArray(c.mappings)) { errors.push(`WEBHOOK_BAD_MAPPINGS: ${who} - mappings - массив [{path,saveTo}].`); break; }
|
|
273
|
+
(c.mappings || []).forEach((m, j) => {
|
|
274
|
+
if (blank(m?.path) || !String(m.path).trim().startsWith("$")) errors.push(`WEBHOOK_BAD_MAPPING_PATH: ${who} - mappings #${j + 1}: path - JsonPath, начинается с $ (например $.data.email).`);
|
|
275
|
+
if (blank(m?.saveTo) || !VAR_RE.test(String(m.saveTo))) errors.push(`WEBHOOK_BAD_MAPPING_SAVE_TO: ${who} - mappings #${j + 1}: saveTo ∈ [a-z_][a-z0-9_]{0,63}.`);
|
|
276
|
+
});
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "ACCESS_GATE": {
|
|
280
|
+
// Зеркало GraphValidator.validateAccessGate: проверка доступа, выходы allowed / denied.
|
|
281
|
+
if (c.match != null && !["ANY", "ALL"].includes(c.match)) errors.push(`ACCESS_GATE_BAD_MATCH: ${who} - match ∈ {ANY, ALL}.`);
|
|
282
|
+
const rules = Array.isArray(c.rules) ? c.rules : [];
|
|
283
|
+
if (rules.length === 0) errors.push(`ACCESS_GATE_NO_RULES: ${who} - нужно хотя бы одно правило в rules.`);
|
|
284
|
+
const GATE_KINDS = ["ALLOWLIST", "TG_ID", "USERNAME", "PHONE", "TAG", "SEGMENT", "PAID_SUBSCRIPTION", "VARIABLE"];
|
|
285
|
+
rules.forEach((r, j) => {
|
|
286
|
+
const k = r?.kind;
|
|
287
|
+
if (!GATE_KINDS.includes(k)) { errors.push(`ACCESS_GATE_BAD_KIND: ${who} - правило #${j + 1}: kind «${k}» не из {${GATE_KINDS.join(",")}}.`); return; }
|
|
288
|
+
if (["TG_ID", "USERNAME", "PHONE"].includes(k) && blank(r.value)) errors.push(`ACCESS_GATE_NO_VALUE: ${who} - правило #${j + 1} (${k}) требует value.`);
|
|
289
|
+
if (k === "SEGMENT" && blank(r.value)) errors.push(`ACCESS_GATE_NO_SEGMENT: ${who} - правило #${j + 1} (SEGMENT) требует value (id сегмента, audience_segments).`);
|
|
290
|
+
if (k === "VARIABLE" && blank(r.key)) errors.push(`ACCESS_GATE_NO_KEY: ${who} - правило #${j + 1} (VARIABLE) требует key.`);
|
|
291
|
+
});
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
255
294
|
case "CALL_WEBHOOK":
|
|
256
295
|
if (blank(c.url) || !/^https?:\/\//.test(c.url)) errors.push(`${who}: url обязателен и http(s)://.`);
|
|
257
296
|
break;
|
|
@@ -424,6 +463,28 @@ for (const n of nodes) {
|
|
|
424
463
|
errors.push(`ACTION_BOOKING_NO_CALENDAR: ${who} — ${a.kind} требует calendarId (booking_calendar_list).`);
|
|
425
464
|
if (["booking_slots", "booking_book"].includes(a.kind) && !blank(a.saveTo) && !VAR_RE.test(String(a.saveTo)))
|
|
426
465
|
errors.push(`ACTION_BAD_KEY: ${who} — ${a.kind}.saveTo ∈ [a-z_][a-z0-9_]{0,63}.`);
|
|
466
|
+
if (a.kind === "publish_post") {
|
|
467
|
+
if (a.target !== "channel" && a.target !== "vk")
|
|
468
|
+
errors.push(`PUBLISH_BAD_TARGET: ${who} — publish_post.target ∈ channel|vk.`);
|
|
469
|
+
else if (blank(a.target === "vk" ? a.accountId : a.chatId))
|
|
470
|
+
errors.push(`PUBLISH_NO_DESTINATION: ${who} — publish_post.${a.target === "vk" ? "accountId" : "chatId"} обязателен.`);
|
|
471
|
+
if (blank(a.text) && blank(a.mediaUrl)) errors.push(`PUBLISH_EMPTY: ${who} — publish_post требует text или mediaUrl.`);
|
|
472
|
+
}
|
|
473
|
+
if (a.kind === "start_broadcast") {
|
|
474
|
+
if (!UUID_RE.test(String(a.draftId || "").trim()))
|
|
475
|
+
errors.push(`BROADCAST_BAD_DRAFT: ${who} — start_broadcast.draftId = UUID черновика рассылки (broadcast_drafts).`);
|
|
476
|
+
if (!blank(a.segmentId) && !UUID_RE.test(String(a.segmentId).trim()))
|
|
477
|
+
errors.push(`BROADCAST_BAD_SEGMENT: ${who} — start_broadcast.segmentId = UUID сохранённого сегмента (audience_segments) или пусто.`);
|
|
478
|
+
if (a.minIntervalHours != null && !(Number.isInteger(a.minIntervalHours) && a.minIntervalHours >= 1 && a.minIntervalHours <= 720))
|
|
479
|
+
errors.push(`BROADCAST_BAD_INTERVAL: ${who} — start_broadcast.minIntervalHours — целое 1..720 (по умолчанию 24).`);
|
|
480
|
+
if (a.idempotencyKey != null && (typeof a.idempotencyKey !== "string" || a.idempotencyKey.length > 100))
|
|
481
|
+
errors.push(`${who}: start_broadcast.idempotencyKey — строка до 100 символов.`);
|
|
482
|
+
}
|
|
483
|
+
if (a.kind === "chain_enqueue") {
|
|
484
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(a.chainId || "").trim()))
|
|
485
|
+
errors.push(`CHAIN_ENQUEUE_BAD_CHAIN: ${who} — chain_enqueue.chainId = UUID цепочки (chain_list).`);
|
|
486
|
+
if (blank(a.text) && blank(a.mediaUrl)) errors.push(`CHAIN_ENQUEUE_EMPTY: ${who} — chain_enqueue требует text или mediaUrl.`);
|
|
487
|
+
}
|
|
427
488
|
if (["add_tag", "remove_tag", "autoflow_add", "autoflow_remove"].includes(a.kind) && !TAG_RE.test(String(a.tag || "")))
|
|
428
489
|
errors.push(`ACTION_BAD_TAG: ${who} — ${a.kind}.tag ∈ [a-z0-9_-]{1,64}.`);
|
|
429
490
|
if (a.kind === "set_field" && !VAR_RE.test(String(a.key || "")))
|
package/src/agent-tools.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Проверка tools[] агента до отправки в agent_update: зеркало AiAgentService.tools + AgentWriteToolConfig
|
|
2
2
|
// на бэкенде (не строже его — иначе MCP отвергнет то, что сервер принял бы). Возвращает список ошибок.
|
|
3
3
|
|
|
4
|
-
const READ = ["AMO_LOOKUP", "SHEETS_LOOKUP", "HTTP_API"];
|
|
5
|
-
const WRITE = ["AMO_CREATE_LEAD", "AMO_UPDATE_LEAD", "AMO_UPDATE_CONTACT", "SHEETS_APPEND", "HTTP_POST", "SET_FIELD", "ADD_TAG", "HANDOFF"];
|
|
4
|
+
const READ = ["AMO_LOOKUP", "SHEETS_LOOKUP", "HTTP_API", "BOOKING_SLOTS"];
|
|
5
|
+
const WRITE = ["AMO_CREATE_LEAD", "AMO_UPDATE_LEAD", "AMO_UPDATE_CONTACT", "SHEETS_APPEND", "HTTP_POST", "SET_FIELD", "ADD_TAG", "HANDOFF", "BOOKING_BOOK", "PAYMENT_LINK", "PUBLISH_POST"];
|
|
6
|
+
// схема аргументов этих инструментов фиксирована сервером
|
|
7
|
+
const FIXED_SCHEMA = ["SET_FIELD", "ADD_TAG", "HANDOFF", "BOOKING_BOOK", "PAYMENT_LINK", "PUBLISH_POST"];
|
|
6
8
|
const ARG_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,39}$/;
|
|
7
9
|
const PROP_KEYS = new Set(["type", "description", "enum", "pattern", "maxLength", "minLength", "minimum", "maximum"]);
|
|
8
10
|
const SCHEMA_KEYS = new Set(["type", "properties", "required", "description"]);
|
|
@@ -54,16 +56,28 @@ export function validateAgentTools(tools) {
|
|
|
54
56
|
ids.add(t.id);
|
|
55
57
|
if (!t.title) errs.push(`${id}: нужен title`);
|
|
56
58
|
if (![...READ, ...WRITE].includes(t.type)) { errs.push(`${id}: неизвестный type ${t.type}`); continue; }
|
|
59
|
+
if (t.type === "BOOKING_SLOTS" && !t.bookingCalendarId) errs.push(`${id}: нужен bookingCalendarId (booking_calendar_list)`);
|
|
57
60
|
if (!WRITE.includes(t.type)) continue;
|
|
58
61
|
for (const [k, max] of [["limitPerConversation", 20], ["limitPerDay", 1000]]) {
|
|
59
62
|
if (t[k] != null && !(Number.isInteger(t[k]) && t[k] >= 1 && t[k] <= max)) errs.push(`${id}: ${k} — число от 1 до ${max}`);
|
|
60
63
|
}
|
|
61
64
|
let known = new Set();
|
|
62
|
-
if (!
|
|
65
|
+
if (!FIXED_SCHEMA.includes(t.type)) known = schemaErrors(t.argsSchema, errs, id);
|
|
63
66
|
if (t.type.startsWith("AMO_") && !t.amoConnectionId) errs.push(`${id}: нужен amoConnectionId (подключение amoCRM)`);
|
|
64
67
|
if (t.type === "AMO_CREATE_LEAD" && !t.amoLeadName) errs.push(`${id}: нужен amoLeadName`);
|
|
65
68
|
if (t.type === "AMO_UPDATE_LEAD" && !t.amoLeadId) errs.push(`${id}: нужен amoLeadId`);
|
|
66
69
|
if (t.type === "AMO_UPDATE_CONTACT" && !t.amoContactId) errs.push(`${id}: нужен amoContactId`);
|
|
70
|
+
if (t.type === "BOOKING_BOOK" && !t.bookingCalendarId) errs.push(`${id}: нужен bookingCalendarId (booking_calendar_list)`);
|
|
71
|
+
if (t.type === "PAYMENT_LINK") {
|
|
72
|
+
if (!t.payConnectionId) errs.push(`${id}: нужен payConnectionId (подключение ЮKassa)`);
|
|
73
|
+
if (!(Number(String(t.payAmount ?? "").replace(",", ".")) > 0)) errs.push(`${id}: payAmount — положительная сумма`);
|
|
74
|
+
if (!t.payDescription) errs.push(`${id}: нужен payDescription`);
|
|
75
|
+
}
|
|
76
|
+
if (t.type === "PUBLISH_POST") {
|
|
77
|
+
if (t.publishTarget === "vk") { if (!t.publishAccountId) errs.push(`${id}: нужен publishAccountId (vk_status)`); }
|
|
78
|
+
else if (t.publishTarget === "channel") { if (!t.publishBotId || t.publishChatId == null) errs.push(`${id}: нужны publishBotId и publishChatId (bot_channel_list)`); }
|
|
79
|
+
else errs.push(`${id}: publishTarget — channel|vk`);
|
|
80
|
+
}
|
|
67
81
|
if (t.type === "SHEETS_APPEND") {
|
|
68
82
|
if (!t.sheetsEmail || !t.sheetsSpreadsheetId) errs.push(`${id}: нужны sheetsEmail и sheetsSpreadsheetId`);
|
|
69
83
|
if (!Array.isArray(t.sheetsRowTemplate) || t.sheetsRowTemplate.length === 0) errs.push(`${id}: нужен sheetsRowTemplate (ячейки)`);
|
package/src/index.mjs
CHANGED
|
@@ -1108,6 +1108,8 @@ const TOOLS = [
|
|
|
1108
1108
|
{ name: "kb_add_text", description: "Добавить источник «Текст/инструкция» в базу знаний (POST /api/bots/kb/{kbId}/docs/text): title (≤120 символов), text (≤100 000 символов). Индексация уходит в фон — документ появится в kb_docs со статусом PENDING → READY. Агент видит найденные фрагменты (~800 знаков; текст до 2500 знаков хранится одним куском), а не документ целиком; чтобы выдавать текст дословно, используй узел сценария RANDOM_TEXT.", inputSchema: { type: "object", properties: { kbId: { type: "string" }, title: { type: "string" }, text: { type: "string" } }, required: ["kbId", "title", "text"] } },
|
|
1109
1109
|
{ name: "kb_add_site", description: "Добавить сайт в базу знаний обходом страниц (POST /api/bots/kb/{kbId}/docs/site): url (http/https, не внутренняя сеть), schedule — расписание повторного обхода: NEVER|DAILY|WEEKLY|MONTHLY (по умолчанию NEVER). Обход уходит в фон; документ появится в kb_docs со статусом PENDING.", inputSchema: { type: "object", properties: { kbId: { type: "string" }, url: { type: "string" }, schedule: { type: "string", enum: ["NEVER", "DAILY", "WEEKLY", "MONTHLY"] } }, required: ["kbId", "url"] } },
|
|
1110
1110
|
{ 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"] } },
|
|
1111
|
+
{ name: "analytics_ask", description: "Вопрос к аналитике обычным русским языком (POST /api/bots/analytics/ask): question ≤ 300 символов, например «сколько новых подписчиков за неделю» или «выручка по источникам за месяц». Бэкенд сам переводит вопрос в вызов белого списка готовых запросов (метрика/бот/период/источник/группировка) только по данным владельца; произвольных запросов к базе нет. Ответ: ok, answer (число), table, method («как посчитано»), plan; ok:false + message, если вопрос не про статистику. Тратит квоту ИИ (ASSIST), лимит 20 вопросов за 10 минут (429).", inputSchema: { type: "object", properties: { question: { type: "string" } }, required: ["question"] } },
|
|
1112
|
+
{ name: "faq_suggestions", description: "Авто-FAQ агента (/api/bots/agents/{agentId}/faq): action — list (ожидающие предложения: question, answer-черновик из базы знаний, count, samples), generate (найти частые вопросы за 30 дней; тратит квоту ИИ), approve (suggestionId, опц. question/answer с правками; добавляет пару в базу знаний как QA, ответ обязателен), dismiss (suggestionId). Предложения никогда не попадают в базу без approve.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, action: { type: "string", enum: ["list", "generate", "approve", "dismiss"] }, suggestionId: { type: "string" }, question: { type: "string" }, answer: { type: "string" } }, required: ["agentId", "action"] } },
|
|
1111
1113
|
{ 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"] } },
|
|
1112
1114
|
{ name: "agent_tests_run", description: "Проверка агента ИИ-клиентами (POST /api/bots/agents/{agentId}/ai-tests/runs): ИИ играет клиентов по сохранённым тест-кейсам (персона + цель + критерии), ведёт диалог с агентом в песочнице, ИИ-судья ставит PASS/FAIL с причиной и цитатой; результат сравнивается с прошлым прогоном (регрессии). ТРАТИТ бюджет ИИ (до 120 вызовов на прогон, до 10 кейсов, до 30 прогонов в сутки; пробные ответы не используются) — поэтому нужен confirm:true. caseIds — какие кейсы гнать (по умолчанию все, максимум 10). wait (по умолчанию true) — дождаться конца прогона (до timeoutSec, по умолчанию 240) и вернуть итог; wait:false вернёт id прогона сразу — дальше agent_tests_list с runId. Кейсы создаёт владелец во вкладке «Проверка» или generate:N (1–10): сначала создаст N кейсов с ИИ (ещё один вызов ИИ), потом прогонит.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, confirm: { type: "boolean", description: "true — подтверждение расхода бюджета ИИ" }, caseIds: { type: "array", items: { type: "string" } }, generate: { type: "number", description: "Создать столько кейсов с ИИ перед прогоном (1–10)" }, wait: { type: "boolean" }, timeoutSec: { type: "number" } }, required: ["agentId", "confirm"] } },
|
|
1113
1115
|
{ name: "agent_tests_list", description: "Кейсы проверки ИИ-клиентами и история прогонов агента (GET /api/bots/agents/{agentId}/ai-tests/cases, /runs, /runs/{runId}): cases — кейсы, history — последние 20 прогонов (итоги без диалогов), run — полный прогон (runId или последний): по каждому кейсу verdict PASS/FAIL/ERROR, reason, quote, диалог, previousVerdict и regression (раньше проходил, теперь нет). Ничего не тратит. Read-only.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, runId: { type: "string" } }, required: ["agentId"] } },
|
|
@@ -1141,7 +1143,14 @@ const TOOLS = [
|
|
|
1141
1143
|
inputSchema: { type: "object", properties: { name: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, botId: { type: "string" }, messages: { type: "array", items: {} }, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } }, audienceFilter: AUDIENCE_FILTER_SCHEMA, scheduledAt: { type: "string", description: "ISO 8601, напр. 2026-10-06T10:00:00+03:00; пусто — сразу" }, graphId: { type: "string", description: "рассылка запуском сценария бота вместо сообщений" }, entryNodeId: { type: "string" }, draftId: { type: "string" }, segmentId: { type: "string", description: "id сохранённого сегмента аудитории (audience_segments); главнее audienceFilter и тегов" }, localTime: { type: "string", description: "HH:mm — слать каждому в это время по его часам (не с dedup/stagger)" }, dedup: { type: "boolean", description: "Не отправлять дважды одному человеку среди нескольких botIds (мульти-бот)" }, stagger: STAGGER_SCHEMA } } },
|
|
1142
1144
|
{ name: "audience_segments", description: "Сохранённые сегменты аудитории рассылок (имя + фильтр; /api/bots/{botId}/segments; общие для всех ботов владельца, до 50). action: list — сегменты {id, name, filter}; create {name, audienceFilter | tagsAll/tagsNone} — сохранить фильтр под именем (до 60 символов); delete {segmentId}. Использовать: broadcast_send / broadcast_preview {segmentId}. botId — любой твой бот.", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "create", "delete"] }, botId: { type: "string" }, name: { type: "string" }, audienceFilter: AUDIENCE_FILTER_SCHEMA, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } }, segmentId: { type: "string" } }, required: ["action", "botId"] } },
|
|
1143
1145
|
{ name: "bot_delivery_window", description: "Тихие часы бота (/api/bots/{botId}/delivery-window): в окно quietFrom..quietTo (HH:mm, может переходить через полночь, например 22:00..09:00) рассылки не уходят, а переносятся на конец окна. Время считается по поясу подписчика (переменная timezone/tz), если неизвестен — по поясу бота timezone (IANA или UTC+3, пусто = Москва). По умолчанию выключено. action: get · set {enabled, quietFrom?, quietTo?, timezone?} (enabled:true требует quietFrom и quietTo, они должны различаться; enabled:false выключает, время сохраняется).", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["get", "set"] }, botId: { type: "string" }, enabled: { type: "boolean" }, quietFrom: { type: "string", description: "HH:mm" }, quietTo: { type: "string", description: "HH:mm" }, timezone: { type: "string", description: "IANA (Europe/Moscow) или UTC+3" } }, required: ["action", "botId"] } },
|
|
1146
|
+
{ name: "bot_access", description: "Закрытый бот и список доступа (/api/bots/{botId}/access). action: get {q?, page?, size?} - настройки (privateBot, deniedMessage) и записи; settings {privateBot, deniedMessage?} - включить/выключить закрытый режим и текст отказа; add {items:[{kind:\"TG_ID\"|\"USERNAME\"|\"PHONE\", value, note?}]} или {text} (многострочный импорт, по записи в строке) - добавить; remove {entryId} - удалить запись; clear {confirm:true} - очистить весь список (без confirm не выполняется). Проверка в сценарии - узел ACCESS_GATE (правило ALLOWLIST смотрит этот список).", inputSchema: { type: "object", properties: { botId: { type: "string" }, action: { type: "string", enum: ["get", "settings", "add", "remove", "clear"] }, privateBot: { type: "boolean" }, deniedMessage: { type: "string" }, items: { type: "array", items: { type: "object", properties: { kind: { type: "string", enum: ["TG_ID", "USERNAME", "PHONE"] }, value: { type: "string" }, note: { type: "string" } }, required: ["kind", "value"] } }, text: { type: "string", description: "многострочный импорт записей" }, entryId: { type: "string" }, confirm: { type: "boolean", description: "обязателен для action=clear" }, q: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["botId", "action"] } },
|
|
1147
|
+
{ name: "preset_list", description: "Пресеты блоков (сохранённые настройки узла: /api/block-presets). Без botId - пресеты аккаунта; с botId - ещё и пресеты этого бота. Секретные значения заголовков (Authorization, токены, ключи) хранятся зашифрованно и возвращаются только ссылками \"preset-secret:<id>:<Header>\" - такую ссылку можно подставлять как значение заголовка в headersJson узла графа, но не открытым текстом.", inputSchema: { type: "object", properties: { botId: { type: "string" } } } },
|
|
1148
|
+
{ name: "preset_save", description: "Сохранить пресет блока: POST /api/block-presets (новый) или PUT /api/block-presets/{presetId} (обновить). name, nodeType (тип узла, напр. CALL_WEBHOOK), config (настройки узла), description?, botId? (пусто = пресет аккаунта, иначе только этого бота). Секретные заголовки в config.headersJson (Authorization, токены, ключи) сервер шифрует и отдаёт только ссылками \"preset-secret:<id>:<Header>\"; ссылку используй как значение заголовка в headersJson графа, не открытый текст. Инструмент не печатает секреты в своём выводе.", inputSchema: { type: "object", properties: { presetId: { type: "string", description: "есть - обновление (PUT)" }, name: { type: "string" }, description: { type: "string" }, nodeType: { type: "string" }, config: { type: "object" }, botId: { type: "string", description: "пусто = пресет аккаунта" } }, required: ["name", "nodeType", "config"] } },
|
|
1149
|
+
{ name: "preset_delete", description: "Удалить пресет блока (DELETE /api/block-presets/{presetId}). Графы, где уже вставлен блок, не меняются.", inputSchema: { type: "object", properties: { presetId: { type: "string" } }, required: ["presetId"] } },
|
|
1150
|
+
{ name: "webhook_requests", description: "Входящие запросы сценария-вебхука (/api/bots/graphs/{graphId}/webhook-requests): последние 50 за 7 дней, секреты замаскированы. action: list (по умолчанию) - показать; listen - включить режим прослушивания (POST .../webhook-listen): открывается тестовый URL /api/webhooks/t/<path>, запрос на него сохраняется и не запускает сценарий; stop - выключить прослушивание (DELETE .../webhook-listen); clear {confirm:true} - стереть журнал. Используй, чтобы увидеть реальное тело запроса и составить TRIGGER_WEBHOOK config.mappings [{path,saveTo}].", inputSchema: { type: "object", properties: { graphId: { type: "string" }, action: { type: "string", enum: ["list", "listen", "stop", "clear"] }, confirm: { type: "boolean", description: "обязателен для action=clear" } }, required: ["graphId"] } },
|
|
1151
|
+
{ name: "http_test", description: "Тестовый HTTP-запрос с сервера (POST /api/http-test; защита от SSRF, таймаут до 15 с, ответ до 1 МБ). url, method? (GET по умолчанию), headersJson? (JSON-строка), bodyTemplate?, timeoutMs?, vars? (подстановки {{var.x}}). Возвращает status, headers, body и fields - список JsonPath-полей ответа: их подставляй в extract:[{path,saveTo}] действия external_request (ACTIONS) или в mappings вебхука. Секреты заголовков в ответе не печатаются.", inputSchema: { type: "object", properties: { url: { type: "string" }, method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] }, headersJson: { type: "string" }, bodyTemplate: { type: "string" }, timeoutMs: { type: "number" }, vars: { type: "object" } }, required: ["url"] } },
|
|
1144
1152
|
{ name: "broadcast_test", description: "Отправить тест рассылки себе (POST /api/bots/{botId}/broadcasts/test): те же сообщения, что у broadcast_send, на твой привязанный Telegram (бот должен быть Telegram, а ты — нажать /start у этого бота; иначе 422 owner_chat_not_linked или 502 от Telegram) либо в чат подписчика chatId этого бота (так тестируют MAX/VK). Мимо квоты, очереди и тихих часов, подписчикам ничего не уходит.", inputSchema: { type: "object", properties: { botId: { type: "string" }, messages: { type: "array", items: {} }, chatId: { type: "number", description: "чат подписчика этого бота; пусто — привязанный Telegram владельца" } }, required: ["botId", "messages"] } },
|
|
1153
|
+
{ name: "broadcast_undo", description: "Отменить УЖЕ ОТПРАВЛЕННУЮ рассылку у получателей — удалить сообщения (фоновая операция с прогрессом и лимитами). Окна: Telegram 48 ч (старше — заглушка «Сообщение удалено», если placeholderForExpired не false), VK 24 ч, MAX без срока. action: preview (по умолчанию; сколько сообщений удалится/просрочено, ничего не меняет) | start (запустить, нужен confirm:true; 202 {opId}) | status (opId; прогресс done/failed/expired/gone/blocked) | pause|resume|cancel (opId). Запланированную/идущую рассылку останавливает broadcast_cancel.", inputSchema: { type: "object", properties: { broadcastId: { type: "string" }, action: { type: "string", enum: ["preview", "start", "status", "pause", "resume", "cancel"] }, opId: { type: "string" }, confirm: { type: "boolean" }, placeholderForExpired: { type: "boolean" } } } },
|
|
1145
1154
|
{ name: "broadcast_cancel", description: "Отменить рассылку (POST /api/bots/broadcasts/{id}/cancel): запланированная не уйдёт, идущая остановится (статус CANCELLING → CANCELLED). Уже завершённую отменить нельзя — HTTP 409. Для группы (dedup/stagger) — broadcast_group {action:\"cancel\"}.", inputSchema: { type: "object", properties: { broadcastId: { type: "string" } }, required: ["broadcastId"] } },
|
|
1146
1155
|
{ name: "broadcast_recurring", description: "Повторяющиеся рассылки (/api/bots/broadcasts/recurring). action: list — правила (активные и остановленные); create {name, botIds[], messages[], tagsAll?/tagsNone? или audienceFilter?, recurrence: DAILY|MONTHLY|YEARLY, firstRunAt, dedup?, stagger?} — сообщения и фильтр как в broadcast_send; dedup/stagger заданы — КАЖДЫЙ прогон правила порождает свою отдельную группу (своя дедупликация с нуля, аудитория у повтора может быть другой); firstRunAt — первый запуск в будущем (ISO 8601, без пояса — московское), дальше в то же время суток/число (Москва); stop {ruleId} — остановить правило (уже отправленные прогоны не трогаются). Нужен платный тариф (402 со ссылкой).", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "create", "stop"] }, ruleId: { type: "string" }, name: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, botId: { type: "string" }, messages: { type: "array", items: {} }, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } }, audienceFilter: AUDIENCE_FILTER_SCHEMA, recurrence: { type: "string", enum: ["DAILY", "MONTHLY", "YEARLY"] }, firstRunAt: { type: "string" }, dedup: { type: "boolean" }, stagger: STAGGER_SCHEMA }, required: ["action"] } },
|
|
1147
1156
|
{ name: "broadcast_drafts", description: "Черновики рассылок (/api/bots/broadcasts/drafts) — те же, что в мастере кабинета. action: list · get {draftId} · create {name?, botIds?, messages?, tagsAll?, tagsNone?, scheduledAt?} · update {draftId, …те же поля — переданные заменяют, остальные остаются} · delete {draftId}. Черновик не проверяется на полноту; отправить — broadcast_send {draftId}. Лимит — 200 черновиков.", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "get", "create", "update", "delete"] }, draftId: { type: "string" }, name: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, messages: { type: "array", items: {} }, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } }, scheduledAt: { type: "string" } }, required: ["action"] } },
|
|
@@ -1190,6 +1199,8 @@ const TOOLS = [
|
|
|
1190
1199
|
{ 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"] } },
|
|
1191
1200
|
{ name: "utm_sources", description: "UTM-источники подписчиков бота (GET /api/bots/{botId}/utm-sources): по каким меткам utm_* люди входили в сценарии и сколько их. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
1192
1201
|
{ name: "source_revenue", description: "Отчёт источник -> выручка по боту (GET /api/bots/{botId}/analytics/source-revenue): по каждому источнику (UTM или стартовая ссылка) подписчики периода, покупатели, выручка (RUB копейки revenueKop + Stars revenueStars), конверсия. Когорта: подписчики за from..to и все их оплаты. Даты YYYY-MM-DD, окно до 92 дней. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, from: { type: "string", description: "YYYY-MM-DD" }, to: { type: "string", description: "YYYY-MM-DD" } }, required: ["botId", "from", "to"] } },
|
|
1202
|
+
{ name: "bot_health", description: "Здоровье бота: статус OK/WARN/CRIT и список проблем с подсказками что делать (GET /api/bots/{botId}/health, свежий расчёт). Коды: TOKEN_REVOKED (токен отозван), TOKEN_CONFLICT (409, токен занят другим сервисом), POLLING_FAILURE (сбои getUpdates), SCENARIO_ERRORS (много упавших прогонов за сутки), PAYMENT_WEBHOOK_FAILURE (сбой обработки платежа), CONVERSION_DROP (конверсия старт->оплата ниже половины недельной нормы), AGENT_UNANSWERED_SPIKE (всплеск вопросов без ответа у ИИ-агента). Без botId - сохранённый статус всех ботов (GET /api/bots/health). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string", description: "Пусто - статус всех ботов из последнего обхода монитора" } } } },
|
|
1203
|
+
{ name: "unsubscribe_reasons", description: "Причины отписки подписчиков бота за период (GET /api/bots/{botId}/unsubscribe-reasons?days): {total, answered, byCode:[{code,count}] (blocked, unspecified, too_often, not_relevant, not_interested, found_other, other), clusters:[{label,count,samples}] (темы свободных ответов, группирует ИИ раз в неделю), recentTexts}. Спрос причины включается в настройках бота (Здоровье и отписки). days 1..365, по умолчанию 30. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, days: { type: "number", description: "1..365, по умолчанию 30" } }, required: ["botId"] } },
|
|
1193
1204
|
{ 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"] } },
|
|
1194
1205
|
{ 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"] } },
|
|
1195
1206
|
{ name: "agent_unpublish", description: "Снять ИИ-агента с публикации (POST /api/bots/agents/{agentId}/unpublish): агент возвращается в черновик и перестаёт отвечать. В ответе — agent и сценарии, которые сейчас на нём работают (их стоит проверить).", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
|
|
@@ -1222,7 +1233,7 @@ const TOOLS = [
|
|
|
1222
1233
|
{ name: "chain_list", description: "Цепочки публикаций пользователя (GET /api/bots/chains): автоматический репост source → граф обработки/назначений. Цепочка привязана к владельцу, не к конкретному боту. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1223
1234
|
{ name: "chain_get", description: "Цепочка по id целиком, как хранится в базе — легаси-поля (source/steps/destinations) ИЛИ нативный граф (nodes/edges), смотря что заполнено (GET /api/bots/chains/{id}). Для гарантированно графовой формы (в т.ч. у старых линейных цепочек) используй chain_graph_get. 404 — не своя/не найдена. Read-only.", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1224
1235
|
{ name: "chain_graph_get", description: "Граф цепочки В ЛЮБОМ случае (GET /api/bots/chains/{id}/graph): {nodes, edges} — если цепочка легаси (линейная), бэкенд синтезирует эквивалентный граф на лету (ничего не сохраняет). Используй это вместо самостоятельной сборки графа из steps/destinations. Read-only.", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1225
|
-
{ name: "chain_create", description: "Создать цепочку публикаций (POST /api/bots/chains): chain — PublishChain draft, {name, enabled?, approvalMode?: NONE|AI_ONLY|ALL, source: {type: TELEGRAM|VK|RSS, botId?, chatId?, accountId?, rssUrl?}, utmDefaults?, И ЛИБО легаси-поля (steps?, destinations?) ЛИБО нативный граф (nodes?, edges?)}. `source` ВСЕГДА отдельное top-level поле — даже в графовом режиме узел SOURCE в nodes его не дублирует, это просто маркер-корень графа. ГРАФ (nodes непустой — приоритетнее steps/destinations, которые тогда игнорируются): nodes — [{id, type: SOURCE|FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY|DESTINATION, x?, y?, step?: {...поля шага, форма как у steps[] в legacy, см. chain_update}, destination?: {id, type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?, aiOverride?} — только для type=DESTINATION}], edges — [{fromNodeId, toNodeId}]. `x`/`y` (number, опционально) — координаты узла для отрисовки на холсте «Схема»; чисто UI-подсказка, бэкенд их не интерпретирует и не валидирует. Опусти (или оставь null) — фронтенд сам раскладывает узлы BFS-авто-layout'ом по столбцам/строкам; не нужно придумывать координаты самостоятельно. `destination.aiOverride` (опционально, та же форма и в легаси destinations[], см. ниже) — персональная адаптация ИИ ТОЛЬКО для этого назначения: {enabled: bool, aiMode: NONE|LIGHT|FULL, tone?: string, instructions?: string}. null/enabled:false (по умолчанию) — ничего не меняется, применяется только общий узел AI_ADAPT на пути к этому назначению (если он есть), как обычно. enabled:true — ПОСЛЕ результата общего узла (или сырого текста источника, если общего AI_ADAPT на пути нет) вызывается ЕЩЁ ОДНА адаптация ИИ с параметрами override, результат идёт ТОЛЬКО в это назначение — соседние назначения под тем же общим узлом не затрагиваются (означает лишний вызов ИИ и лишнее списание только за это назначение). Используй, когда нужен один общий тон для большинства каналов, но для одного-двух — особый (напр. более формальный) без заведения отдельного AI_ADAPT узла в графе. Модель — дерево-с-ветвлением (out-tree), НЕ произвольный DAG: у любого не-SOURCE узла ровно ОДНО входящее ребро; ветвление — через НЕСКОЛЬКО исходящих у одного родителя (напр. один общий AI_ADAPT → много DESTINATION = «1 источник → общий ИИ → N каналов»); слияния путей (несколько родителей) нет. Перед сохранением сложного графа прогони chain_validate (клиентское зеркало серверных правил) и chain_destinations_catalog (список доступных назначений). Для быстрого создания черновика без источника/назначений (поток «создать → сразу в Схему») используй chain_create_draft вместо этого инструмента. ЛЕГАСИ (nodes пуст): destinations: [{type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?, aiOverride?}], steps?: [{type: FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY, ...поля шага}]. `aiOverride` работает в легаси-режиме так же, как в графовом (см. выше) — применяется поверх общего шага AI_ADAPT в steps[], если он есть. TELEGRAM/MAX источник или назначение — свой привязанный канал бота (bot_channel_list/list_channels), VK — подключённый аккаунт (vk_status); chain_destinations_catalog отдаёт оба списка сразу. INSTAGRAM в destinations принимается моделью, но публикация требует медиа и подключённый Instagram-аккаунт — без них задание уходит в SKIPPED с причиной, не FAILED. HTTP 400 — не хватает полей, источник/назначение не найдены/не ваши, ИЛИ (графовый режим) структурная ошибка графа: «Схема цепочки: <причины>» (нет/>1 SOURCE, цикл, узел без входящего ребра или с >1 входящих, DESTINATION с исходящими рёбрами, недостижимое DESTINATION) — ровно те же правила, что проверяет chain_validate. HTTP 409 — прямой цикл с другой вашей цепочкой (A→B при уже существующей B→A).", inputSchema: { type: "object", properties: { chain: { type: "object", description: "PublishChain draft — см. описание инструмента" } }, required: ["chain"] } },
|
|
1236
|
+
{ name: "chain_create", description: "Создать цепочку публикаций (POST /api/bots/chains): chain — PublishChain draft, {name, enabled?, approvalMode?: NONE|AI_ONLY|ALL, source: {type: TELEGRAM|VK|RSS, botId?, chatId?, accountId?, rssUrl?}, utmDefaults?, И ЛИБО легаси-поля (steps?, destinations?) ЛИБО нативный граф (nodes?, edges?)}. `source` ВСЕГДА отдельное top-level поле — даже в графовом режиме узел SOURCE в nodes его не дублирует, это просто маркер-корень графа. ГРАФ (nodes непустой — приоритетнее steps/destinations, которые тогда игнорируются): nodes — [{id, type: SOURCE|FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY|DESTINATION, x?, y?, step?: {...поля шага, форма как у steps[] в legacy, см. chain_update}, destination?: {id, type: TELEGRAM|MAX|VK|INSTAGRAM|BOT_SUBSCRIBERS, botId?, chatId?, accountId?, segmentId?, notifyPrefix?, minIntervalHours?, utmOverride?, aiOverride?} — только для type=DESTINATION}], edges — [{fromNodeId, toNodeId}]. `x`/`y` (number, опционально) — координаты узла для отрисовки на холсте «Схема»; чисто UI-подсказка, бэкенд их не интерпретирует и не валидирует. Опусти (или оставь null) — фронтенд сам раскладывает узлы BFS-авто-layout'ом по столбцам/строкам; не нужно придумывать координаты самостоятельно. `destination.aiOverride` (опционально, та же форма и в легаси destinations[], см. ниже) — персональная адаптация ИИ ТОЛЬКО для этого назначения: {enabled: bool, aiMode: NONE|LIGHT|FULL, tone?: string, instructions?: string}. null/enabled:false (по умолчанию) — ничего не меняется, применяется только общий узел AI_ADAPT на пути к этому назначению (если он есть), как обычно. enabled:true — ПОСЛЕ результата общего узла (или сырого текста источника, если общего AI_ADAPT на пути нет) вызывается ЕЩЁ ОДНА адаптация ИИ с параметрами override, результат идёт ТОЛЬКО в это назначение — соседние назначения под тем же общим узлом не затрагиваются (означает лишний вызов ИИ и лишнее списание только за это назначение). Используй, когда нужен один общий тон для большинства каналов, но для одного-двух — особый (напр. более формальный) без заведения отдельного AI_ADAPT узла в графе. Модель — дерево-с-ветвлением (out-tree), НЕ произвольный DAG: у любого не-SOURCE узла ровно ОДНО входящее ребро; ветвление — через НЕСКОЛЬКО исходящих у одного родителя (напр. один общий AI_ADAPT → много DESTINATION = «1 источник → общий ИИ → N каналов»); слияния путей (несколько родителей) нет. Перед сохранением сложного графа прогони chain_validate (клиентское зеркало серверных правил) и chain_destinations_catalog (список доступных назначений). Для быстрого создания черновика без источника/назначений (поток «создать → сразу в Схему») используй chain_create_draft вместо этого инструмента. ЛЕГАСИ (nodes пуст): destinations: [{type: TELEGRAM|MAX|VK|INSTAGRAM|BOT_SUBSCRIBERS, botId?, chatId?, accountId?, segmentId?, notifyPrefix?, minIntervalHours?, utmOverride?, aiOverride?}], steps?: [{type: FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY, ...поля шага}]. `aiOverride` работает в легаси-режиме так же, как в графовом (см. выше) — применяется поверх общего шага AI_ADAPT в steps[], если он есть. TELEGRAM/MAX источник или назначение — свой привязанный канал бота (bot_channel_list/list_channels), VK — подключённый аккаунт (vk_status); chain_destinations_catalog отдаёт оба списка сразу. BOT_SUBSCRIBERS — не площадка, а оповещение подписчиков бота: новый пост уходит рассылкой бота botId (segmentId — сохранённый сегмент, пусто = все подписчики; notifyPrefix — приписка до 300 символов; minIntervalHours 1..168, по умолчанию 6 — не чаще одного оповещения за срок); работают тихие часы, лимиты тарифа и дедупликация движка рассылок. INSTAGRAM в destinations принимается моделью, но публикация требует медиа и подключённый Instagram-аккаунт — без них задание уходит в SKIPPED с причиной, не FAILED. HTTP 400 — не хватает полей, источник/назначение не найдены/не ваши, ИЛИ (графовый режим) структурная ошибка графа: «Схема цепочки: <причины>» (нет/>1 SOURCE, цикл, узел без входящего ребра или с >1 входящих, DESTINATION с исходящими рёбрами, недостижимое DESTINATION) — ровно те же правила, что проверяет chain_validate. HTTP 409 — прямой цикл с другой вашей цепочкой (A→B при уже существующей B→A).", inputSchema: { type: "object", properties: { chain: { type: "object", description: "PublishChain draft — см. описание инструмента" } }, required: ["chain"] } },
|
|
1226
1237
|
{ name: "chain_create_draft", description: "Создать минимальный черновик цепочки (POST /api/bots/chains/draft) по имени: {name}. Возвращает PublishChain с enabled=false, source={type:TELEGRAM} БЕЗ конкретного botId/chatId, без steps/destinations/nodes/edges — обходит обычную валидацию chain_create (которая потребовала бы источник и ≥1 назначение). Только для потока «новая цепочка → сразу открыть на холсте „Схема“ и донастроить источник/назначения там» — черновик не может случайно уйти в работу недонастроенным (enabled всегда false, обычный chain_update с теми же правилами валидации, что и chain_create, не даст сохранить его без источника и назначений). Для обычного создания цепочки сразу с источником и назначениями используй chain_create.", inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } },
|
|
1227
1238
|
{ name: "chain_update", description: "Изменить цепочку целиком (PUT /api/bots/chains/{id}): chain — PublishChain draft (та же форма, что в chain_create, легаси ИЛИ граф), заменяет текущую конфигурацию полностью — сначала прочитай chain_get (или chain_graph_get, если нужен гарантированно графовый вид) и поменяй нужное. Те же 400/409, что у chain_create (включая x?/y? на узлах и aiOverride? на destination — см. chain_create).", inputSchema: { type: "object", properties: { chainId: { type: "string" }, chain: { type: "object", description: "PublishChain draft целиком" } }, required: ["chainId", "chain"] } },
|
|
1228
1239
|
{ name: "chain_delete", description: "УДАЛИТЬ цепочку публикаций (DELETE /api/bots/chains/{id}). История заданий (chain_runs) теряется вместе с ней. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { chainId: { type: "string" }, confirm: { type: "boolean" } }, required: ["chainId", "confirm"] } },
|
|
@@ -1598,6 +1609,21 @@ async function handleCall(params) {
|
|
|
1598
1609
|
return okResult(await api(`/api/bots/kb/${a.kbId}/docs/site`, { method: "POST", body: { url: a.url, schedule: a.schedule } }));
|
|
1599
1610
|
}
|
|
1600
1611
|
case "kb_reindex": return okResult(await api(`/api/bots/kb/${a.kbId}/docs/${a.docId}/reindex`, { method: "POST", body: { headerRow: a.headerRow } }));
|
|
1612
|
+
case "analytics_ask": {
|
|
1613
|
+
if (!a.question || !String(a.question).trim()) throw new Error("Передай question.");
|
|
1614
|
+
return okResult(await api(`/api/bots/analytics/ask`, { method: "POST", body: { question: String(a.question) } }));
|
|
1615
|
+
}
|
|
1616
|
+
case "faq_suggestions": {
|
|
1617
|
+
const base = `/api/bots/agents/${a.agentId}/faq`;
|
|
1618
|
+
if (a.action === "list") return okResult(await api(base));
|
|
1619
|
+
if (a.action === "generate") return okResult(await api(`${base}/generate`, { method: "POST", body: {} }));
|
|
1620
|
+
if (a.action === "approve" || a.action === "dismiss") {
|
|
1621
|
+
if (!a.suggestionId) throw new Error("Передай suggestionId.");
|
|
1622
|
+
const body = a.action === "approve" ? { question: a.question, answer: a.answer } : {};
|
|
1623
|
+
return okResult(await api(`${base}/${a.suggestionId}/${a.action}`, { method: "POST", body }));
|
|
1624
|
+
}
|
|
1625
|
+
throw new Error("action: list | generate | approve | dismiss.");
|
|
1626
|
+
}
|
|
1601
1627
|
case "agent_unanswered": {
|
|
1602
1628
|
const qs = a.days != null ? `?days=${encodeURIComponent(a.days)}` : "";
|
|
1603
1629
|
return okResult(await api(`/api/bots/agents/${a.agentId}/unanswered${qs}`));
|
|
@@ -1858,6 +1884,19 @@ async function handleCall(params) {
|
|
|
1858
1884
|
await api(`/api/bots/links/${linkId}`, { method: "DELETE" });
|
|
1859
1885
|
return okResult(`🗑️ Ссылка ${linkId} удалена.`);
|
|
1860
1886
|
}
|
|
1887
|
+
case "bot_health": {
|
|
1888
|
+
if (a.botId == null || a.botId === "") return okResult(await api("/api/bots/health"));
|
|
1889
|
+
return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/health`));
|
|
1890
|
+
}
|
|
1891
|
+
case "unsubscribe_reasons": {
|
|
1892
|
+
const botId = uuidArg(a.botId, "botId");
|
|
1893
|
+
let days = 30;
|
|
1894
|
+
if (a.days != null && a.days !== "") {
|
|
1895
|
+
days = Number(a.days);
|
|
1896
|
+
if (!Number.isInteger(days) || days < 1 || days > 365) throw new Error("days: целое число от 1 до 365.");
|
|
1897
|
+
}
|
|
1898
|
+
return okResult(await api(`/api/bots/${botId}/unsubscribe-reasons?days=${days}`));
|
|
1899
|
+
}
|
|
1861
1900
|
case "source_revenue": {
|
|
1862
1901
|
const d = (v, n) => { if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v)) throw new Error(`${n}: нужна дата YYYY-MM-DD`); return v; };
|
|
1863
1902
|
return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/analytics/source-revenue?from=${d(a.from, "from")}&to=${d(a.to, "to")}`));
|
|
@@ -2292,12 +2331,97 @@ async function handleCall(params) {
|
|
|
2292
2331
|
}
|
|
2293
2332
|
throw new Error("action: get | set.");
|
|
2294
2333
|
}
|
|
2334
|
+
case "bot_access": {
|
|
2335
|
+
const botId = uuidArg(a.botId, "botId");
|
|
2336
|
+
const base = `/api/bots/${botId}/access`;
|
|
2337
|
+
if (a.action === "get") {
|
|
2338
|
+
const q = [];
|
|
2339
|
+
if (a.q) q.push(`q=${encodeURIComponent(a.q)}`);
|
|
2340
|
+
if (a.page != null) q.push(`page=${encodeURIComponent(a.page)}`);
|
|
2341
|
+
if (a.size != null) q.push(`size=${encodeURIComponent(a.size)}`);
|
|
2342
|
+
return okResult(await api(base + (q.length ? `?${q.join("&")}` : "")));
|
|
2343
|
+
}
|
|
2344
|
+
if (a.action === "settings") {
|
|
2345
|
+
if (typeof a.privateBot !== "boolean") throw new Error("privateBot: true/false.");
|
|
2346
|
+
return okResult(await api(`${base}/settings`, { method: "PUT", body: { privateBot: a.privateBot, deniedMessage: a.deniedMessage ?? null } }));
|
|
2347
|
+
}
|
|
2348
|
+
if (a.action === "add") {
|
|
2349
|
+
const hasItems = Array.isArray(a.items) && a.items.length > 0;
|
|
2350
|
+
if (!hasItems && !String(a.text || "").trim()) throw new Error("Передай items [{kind,value,note?}] или text (по записи в строке).");
|
|
2351
|
+
for (const [i, it] of (hasItems ? a.items : []).entries()) {
|
|
2352
|
+
if (!["TG_ID", "USERNAME", "PHONE"].includes(it?.kind) || !String(it?.value ?? "").trim()) throw new Error(`items[${i}]: kind ∈ TG_ID|USERNAME|PHONE и непустой value.`);
|
|
2353
|
+
}
|
|
2354
|
+
return okResult(await api(`${base}/entries`, { method: "POST", body: hasItems ? { items: a.items } : { text: String(a.text) } }));
|
|
2355
|
+
}
|
|
2356
|
+
if (a.action === "remove") {
|
|
2357
|
+
const entryId = uuidArg(a.entryId, "entryId");
|
|
2358
|
+
return okResult((await api(`${base}/entries/${entryId}`, { method: "DELETE" })) ?? `Запись ${entryId} удалена.`);
|
|
2359
|
+
}
|
|
2360
|
+
if (a.action === "clear") {
|
|
2361
|
+
if (a.confirm !== true) throw new Error("clear удаляет весь список доступа - передай confirm:true.");
|
|
2362
|
+
return okResult((await api(`${base}/entries`, { method: "DELETE" })) ?? "Список доступа очищен.");
|
|
2363
|
+
}
|
|
2364
|
+
throw new Error("action: get | settings | add | remove | clear.");
|
|
2365
|
+
}
|
|
2366
|
+
case "preset_list": {
|
|
2367
|
+
const qs = a.botId ? `?botId=${uuidArg(a.botId, "botId")}` : "";
|
|
2368
|
+
return okResult(withoutSecrets(await api(`/api/block-presets${qs}`)));
|
|
2369
|
+
}
|
|
2370
|
+
case "preset_save": {
|
|
2371
|
+
if (!String(a.name || "").trim()) throw new Error("name обязателен.");
|
|
2372
|
+
if (!String(a.nodeType || "").trim()) throw new Error("nodeType обязателен (например CALL_WEBHOOK).");
|
|
2373
|
+
if (!a.config || typeof a.config !== "object" || Array.isArray(a.config)) throw new Error("config - объект настроек узла.");
|
|
2374
|
+
const body = { name: String(a.name).trim(), description: a.description ?? null, nodeType: String(a.nodeType).trim(), config: a.config, botId: a.botId ? uuidArg(a.botId, "botId") : null };
|
|
2375
|
+
const r = a.presetId
|
|
2376
|
+
? await api(`/api/block-presets/${uuidArg(a.presetId, "presetId")}`, { method: "PUT", body })
|
|
2377
|
+
: await api("/api/block-presets", { method: "POST", body });
|
|
2378
|
+
return okResult(withoutSecrets(r));
|
|
2379
|
+
}
|
|
2380
|
+
case "preset_delete": {
|
|
2381
|
+
const id = uuidArg(a.presetId, "presetId");
|
|
2382
|
+
await api(`/api/block-presets/${id}`, { method: "DELETE" });
|
|
2383
|
+
return okResult(`Пресет ${id} удалён.`);
|
|
2384
|
+
}
|
|
2385
|
+
case "webhook_requests": {
|
|
2386
|
+
const base = `/api/bots/graphs/${uuidArg(a.graphId, "graphId")}`;
|
|
2387
|
+
const act = a.action || "list";
|
|
2388
|
+
if (act === "list") return okResult(await api(`${base}/webhook-requests`));
|
|
2389
|
+
if (act === "listen") return okResult(await api(`${base}/webhook-listen`, { method: "POST" }));
|
|
2390
|
+
if (act === "stop") return okResult((await api(`${base}/webhook-listen`, { method: "DELETE" })) ?? "Прослушивание выключено.");
|
|
2391
|
+
if (act === "clear") {
|
|
2392
|
+
if (a.confirm !== true) throw new Error("clear стирает журнал запросов - передай confirm:true.");
|
|
2393
|
+
return okResult((await api(`${base}/webhook-requests`, { method: "DELETE" })) ?? "Журнал очищен.");
|
|
2394
|
+
}
|
|
2395
|
+
throw new Error("action: list | listen | stop | clear.");
|
|
2396
|
+
}
|
|
2397
|
+
case "http_test": {
|
|
2398
|
+
if (!/^https?:\/\//i.test(String(a.url || ""))) throw new Error("url обязателен и начинается с http(s)://.");
|
|
2399
|
+
const body = { url: a.url, method: a.method || "GET", headersJson: a.headersJson ?? null, bodyTemplate: a.bodyTemplate ?? null, timeoutMs: a.timeoutMs ?? null, vars: a.vars ?? {} };
|
|
2400
|
+
return okResult(await api("/api/http-test", { method: "POST", body }));
|
|
2401
|
+
}
|
|
2295
2402
|
case "broadcast_test": {
|
|
2296
2403
|
if (!a.botId) throw new Error("Передай botId (list_bots).");
|
|
2297
2404
|
const body = { messages: normalizeBroadcastMessages(a.messages), chatId: a.chatId == null ? null : Number(a.chatId) };
|
|
2298
2405
|
if (body.chatId != null && !Number.isFinite(body.chatId)) throw new Error("chatId — число.");
|
|
2299
2406
|
return okResult(await api(`/api/bots/${a.botId}/broadcasts/test`, { method: "POST", body }));
|
|
2300
2407
|
}
|
|
2408
|
+
case "broadcast_undo": {
|
|
2409
|
+
const act = a.action || "preview";
|
|
2410
|
+
if (["status", "pause", "resume", "cancel"].includes(act)) {
|
|
2411
|
+
if (!a.opId) throw new Error("Передай opId (из ответа action=start).");
|
|
2412
|
+
if (act === "status") return okResult(await api(`/api/bots/broadcasts/ops/${a.opId}`));
|
|
2413
|
+
await api(`/api/bots/broadcasts/ops/${a.opId}/${act}`, { method: "POST" });
|
|
2414
|
+
return okResult(`Операция ${a.opId}: ${act} выполнено.`);
|
|
2415
|
+
}
|
|
2416
|
+
if (!a.broadcastId) throw new Error("Передай broadcastId.");
|
|
2417
|
+
const body = { type: "DELETE", placeholderForExpired: a.placeholderForExpired !== false };
|
|
2418
|
+
if (act === "start") {
|
|
2419
|
+
if (a.confirm !== true) throw new Error("Удаление необратимо: сначала action=preview, затем start с confirm:true.");
|
|
2420
|
+
body.confirm = true;
|
|
2421
|
+
return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/ops`, { method: "POST", body }));
|
|
2422
|
+
}
|
|
2423
|
+
return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/ops/preview`, { method: "POST", body }));
|
|
2424
|
+
}
|
|
2301
2425
|
case "broadcast_cancel":
|
|
2302
2426
|
await api(`/api/bots/broadcasts/${a.broadcastId}/cancel`, { method: "POST" });
|
|
2303
2427
|
return okResult(`⏹️ Рассылка ${a.broadcastId} отменяется (CANCELLING → CANCELLED).`);
|