@retensy/mcp 0.49.0 → 0.51.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.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/agent-write-tools.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs && node scripts/token-scopes.test.mjs && node scripts/secondary-graphs.test.mjs && node scripts/graph-versions.test.mjs && node scripts/templates-filter.test.mjs && node scripts/agent-tests.test.mjs && node scripts/catalog.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/agent-write-tools.test.mjs && node scripts/battery-tools.test.mjs && node scripts/parity-tools.test.mjs && node scripts/parity-2.test.mjs && node scripts/validate-graph.test.mjs && node scripts/call-scenario.test.mjs && node scripts/audience-publications.test.mjs && node scripts/chains-analytics.test.mjs && node scripts/platform-decisions.test.mjs && node scripts/token-scopes.test.mjs && node scripts/secondary-graphs.test.mjs && node scripts/graph-versions.test.mjs && node scripts/templates-filter.test.mjs && node scripts/agent-tests.test.mjs && node scripts/catalog.test.mjs && node scripts/confirm-409.test.mjs",
|
|
25
25
|
"validate": "node skills/build-bot-funnel/validate.mjs"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
@@ -42,6 +42,7 @@ description: Собрать воронку (сценарий) бота для с
|
|
|
42
42
|
- **Оплата звёздами Telegram (Stars, XTR)** — действие `issue_stars_invoice` внутри `ACTIONS`, ТОЛЬКО для Telegram-ботов (на MAX/VK/Instagram/WEB публикация даст `STARS_TELEGRAM_ONLY`). Кассы и `connectionId` нет: `{ "kind":"issue_stars_invoice", "amount":"150", "title":"Курс", "description":"Доступ", "timeoutMinutes":60 }` (`amount` — целое 1…10000, `title` до 32 знаков). Выходы и правила те же, что у `issue_invoice`: `paid`/`timeout`, счёт последним в блоке, один счёт на блок (ЮKassa и звёзды вместе тоже нельзя). После оплаты в переменных `{{var.stars_charge_id}}` (нужен для возврата), `{{var.stars_amount}}`, `{{var.stars_paid_at}}`; история и возврат — в кабинете «Платежи звёздами» (/bots/stars). Разрешение `pre_checkout_query` включается автоматически при публикации.
|
|
43
43
|
Счёт обязан быть **последним** действием в блоке и только одним (`INVOICE_NOT_LAST`, `INVOICE_DUPLICATE`).
|
|
44
44
|
- **`TRIGGER_PAYMENT` («оплата прошла»)** — если счёт выставлял бот, подписчик определяется САМ (счёт помнит его в metadata платежа): можно сразу слать сообщение и вешать метку, разбирать ответ кассы не нужно. Если оплата пришла мимо бота — прогон headless, получателя задавай через `config.target`. Ставь фильтр `minAmount`/`descriptionContains`, иначе сценарий сработает на любую оплату в кассе.
|
|
45
|
+
- **`TRIGGER_ORDER_CREATED` («Заказ создан») / `TRIGGER_ORDER_PAID` («Заказ оплачен»)** — `{siteId?, channel?: "site"|"bot"}`: срабатывает на заказ владельца (оформление / оплата) для каждого опубликованного сценария с этим триггером. Переменные `{{order.number|total|currency|status|name|phone|email|comment|delivery|address|items|channel|id}}`. Типовой кейс — бэкенд сайта: сценарий без бота (WEBHOOK) → `send_email` покупателю, уведомление владельцу, `external_request` в `ACTIONS`.
|
|
45
46
|
- **`TRIGGER_SCHEDULE` («По расписанию»)** — `{cron:"0 9 * * 1-5", timezone}`: headless-прогон без чата (отправка — только с `config.target`), для сводок, эскалаций, SLA. Журнал — `scenario_runs`.
|
|
46
47
|
- **Запись на слоты и напоминание**: календарь создаётся `booking_calendar_create`; в `ACTIONS` — `booking_slots` (список в `{{var.slots}}`) → вопрос → `booking_book` (ветка `taken` — слот занят) → `DELAY UNTIL {{var.slot_at}}` с `offset:"-24h"` → напоминание; `booking_cancel` освобождает слот. **Подписки**: `invite_link_create`/`invite_link_revoke`, `subscription_extend`/`subscription_check`, `yookassa_charge_saved`. **Передача оператору** — `agent_chat` (бот замолкает до «Вернуть боту»). Поля — в schema.md.
|
|
47
48
|
- **Переиспользуемые подсценарии — `call_scenario`** (действие в `ACTIONS`, ставь ПОСЛЕДНИМ в блоке): `graphId` — сценарий ЭТОГО ЖЕ бота (`list_graphs`), `mode: "goto"` (по умолчанию, уйти без возврата) или `"call"` (вызвать и вернуться — после `END`/конца цели движок продолжает вызывающего с места после блока). Общая глубина `goto`+`call` в одной цепочке — не больше 5 переходов/вызовов подряд. Коды ошибок и рантайм-поведение — в validation.md.
|
|
@@ -111,6 +111,8 @@
|
|
|
111
111
|
`{{var.<имя>}}`, всегда `{{var.name}}`/`{{var.email}}`/`{{var.phone}}` (по имени поля, иначе по подписи),
|
|
112
112
|
UTM `{{var.utm_*}}`. Прогон без чата: узлы отправки — только с `config.target`. Работает и в сценарии
|
|
113
113
|
без бота (источник WEBHOOK — тогда `TRIGGER_WEBHOOK` не обязателен), и в сценарии бота; не для Instagram.
|
|
114
|
+
- `TRIGGER_ORDER_CREATED` («Заказ создан») — `{ "siteId": "<uuid сайта, необязательно = любой>", "channel": "site"|"bot" }` — заказ создан (оформлен в корзине сайта или в боте). Срабатывает для КАЖДОГО опубликованного сценария владельца с этим триггером, фильтры — `siteId` и `channel` (пусто = без фильтра). Для бэкенда сайта: письмо, уведомление владельцу, `external_request` в `ACTIONS`. Переменные: `{{order.number}}`, `{{order.total}}`, `{{order.currency}}`, `{{order.status}}`, `{{order.name}}`, `{{order.phone}}`, `{{order.email}}`, `{{order.comment}}`, `{{order.delivery}}`, `{{order.address}}`, `{{order.items}}`, `{{order.channel}}`, `{{order.id}}`.
|
|
115
|
+
- `TRIGGER_ORDER_PAID` («Заказ оплачен») — то же, но на оплату заказа; те же `config` и `{{order.*}}`, `{{order.status}}` — статус после оплаты.
|
|
114
116
|
- `TRIGGER_SCHEDULE` — `{ "cron": "0 9 * * 1-5", "timezone": "Europe/Moscow" }` — запуск сценария по расписанию:
|
|
115
117
|
cron из 5 полей (минута час день месяц день-недели), не чаще раза в минуту; зона по умолчанию `Europe/Moscow`.
|
|
116
118
|
Прогон **headless** (без чата): узлы отправки — только с явным `config.target`, действия, которым нужен
|
|
@@ -246,6 +246,11 @@ for (const n of nodes) {
|
|
|
246
246
|
else if (!UUID_RE.test(String(c.siteId).trim())) errors.push(`SITE_FORM_NO_SITE: ${who} — siteId «${c.siteId}» не UUID сайта (site_list).`);
|
|
247
247
|
if (c.formId != null && typeof c.formId !== "string") errors.push(`${who}: formId — строка (id формы из site_get) или пусто = любая форма.`);
|
|
248
248
|
break;
|
|
249
|
+
case "TRIGGER_ORDER_CREATED":
|
|
250
|
+
case "TRIGGER_ORDER_PAID":
|
|
251
|
+
if (c.channel != null && c.channel !== "" && !["site", "bot"].includes(c.channel)) errors.push(`${who}: channel ∈ {site,bot} или пусто.`);
|
|
252
|
+
if (!blank(c.siteId) && !UUID_RE.test(String(c.siteId).trim())) errors.push(`${who}: siteId — UUID сайта (site_list) или пусто = любой.`);
|
|
253
|
+
break;
|
|
249
254
|
case "TRIGGER_BOOKING": {
|
|
250
255
|
const EVENTS = ["booked", "confirmed", "cancelled", "reminder", "after_visit"];
|
|
251
256
|
if (!Array.isArray(c.events) || c.events.length === 0) {
|
package/src/catalog.mjs
CHANGED
|
@@ -150,14 +150,27 @@ function hash(s) {
|
|
|
150
150
|
return h.toString(16).padStart(8, "0");
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/** Что изменится: summary.text от сервера (diff узлов/блоков, получатели), иначе общий message. */
|
|
154
|
+
function changeSummary(data) {
|
|
155
|
+
const sum = data?.step?.confirm?.summary;
|
|
156
|
+
if (!sum || typeof sum !== "object") return "";
|
|
157
|
+
const parts = [];
|
|
158
|
+
if (typeof sum.text === "string" && sum.text) parts.push(sum.text);
|
|
159
|
+
if (typeof sum.recipients === "number") parts.push(`Получателей: ${sum.recipients}.`);
|
|
160
|
+
if (sum.irreversible === true) parts.push("Действие необратимо.");
|
|
161
|
+
return parts.join(" ");
|
|
162
|
+
}
|
|
163
|
+
|
|
153
164
|
/** Ответ invoke → результат MCP. 409 needsConfirmation — не ошибка сервиса, а просьба спросить человека. */
|
|
154
165
|
export function confirmationText(data) {
|
|
155
166
|
if (data && typeof data === "object" && data.needsConfirmation === true) {
|
|
156
167
|
const msg = typeof data.message === "string" ? data.message : "";
|
|
168
|
+
const what = changeSummary(data);
|
|
169
|
+
const head = ["Нужно подтверждение пользователя.", what && !msg.includes(what) ? `Что изменится: ${what}` : "", msg].filter(Boolean).join(" ");
|
|
157
170
|
if (typeof data.confirmToken === "string" && data.confirmToken) {
|
|
158
|
-
return `⚠️
|
|
171
|
+
return `⚠️ ${head} Опиши пользователю, что именно будет сделано, получи его явное согласие и повтори вызов с ТЕМИ ЖЕ аргументами плюс confirmToken: "${data.confirmToken}" (токен одноразовый, привязан к этим аргументам и действует 5 минут; если состояние изменилось, придёт новый запрос — покажи пользователю новое описание).`.trim();
|
|
159
172
|
}
|
|
160
|
-
return `⚠️
|
|
173
|
+
return `⚠️ ${head}`.trim();
|
|
161
174
|
}
|
|
162
175
|
return null;
|
|
163
176
|
}
|
package/src/index.mjs
CHANGED
|
@@ -1265,7 +1265,7 @@ const TOOLS = [
|
|
|
1265
1265
|
{ name: "site_lead_file_download", description: "Метаданные и ссылка на скачивание файла из заявки сайта (GET /api/bots/pages/{siteId}/leads/{leadId}/files/{fileId}, владелец/ADMIN сайта). Байты файла НЕ передаются через MCP (может быть большим/бинарным) — инструмент делает HEAD-запрос, чтобы подтвердить, что файл принадлежит этой заявке (404 иначе), и вернуть {fileId, fileName, contentType, sizeBytes, downloadUrl}. downloadUrl отдаёт файл только с тем же токеном, что у этого MCP (заголовок Authorization: Bearer zmcp_...), — скачай curl'ом с этим заголовком или открой заявку в кабинете (сайт → «Заявки»). Список файлов заявки с теми же полями уже есть в site_leads (поле files[] каждой заявки) — этот инструмент нужен, только когда нужна именно прямая ссылка на скачивание одного файла.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, leadId: { type: "string" }, fileId: { type: "string" } }, required: ["siteId", "leadId", "fileId"] } },
|
|
1266
1266
|
// ---- Вебхук-сценарии (/api/scenarios, PAT-доступ открыт решением владельца 14.10) ----
|
|
1267
1267
|
{ name: "scenario_list", description: "Сценарии автора (GET /api/scenarios): [{id, name, status, active, sourceType: BOT|WEBHOOK, botId}] — включая вебхук-сценарии (в list_graphs их нет, см. его описание). sourceType WEBHOOK — свой вход по адресу (path+secret в scenario_get), выход — произвольный бот через scenario_update_source; BOT — живёт в графах привязанного бота (тот же объект, что и в list_graphs/get_graph этого бота). Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1268
|
-
{ name: "scenario_create", description: "Создать сценарий (POST /api/scenarios), пустым — узлы/рёбра потом через update_graph/edit_graph_live по id из ответа. botId не передан → вебхук-сценарий (source.type WEBHOOK): сервер сам выпускает путь входа и секрет (видны в scenario_get, ротируются scenario_rotate_secret), выходной бот задаётся отдельно через scenario_update_source. botId передан → бот-сценарий (source.type BOT), сразу привязан к этому боту — нужен доступ на редактирование его графов (owner/EDITOR/ADMIN), иначе 403/404; публикуется он уже через publish_graph, не через scenario_publish. Лимит сценариев тарифа — HTTP 402.", inputSchema: { type: "object", properties: { name: { type: "string" }, botId: { type: "string", description: "UUID бота — делает сценарий бот-сценарием вместо вебхук-сценария" } }, required: ["name"] } },
|
|
1268
|
+
{ name: "scenario_create", description: "Создать сценарий (POST /api/scenarios), пустым — узлы/рёбра потом через update_graph/edit_graph_live по id из ответа. botId не передан → вебхук-сценарий (source.type WEBHOOK): сервер сам выпускает путь входа и секрет (видны в scenario_get, ротируются scenario_rotate_secret), выходной бот задаётся отдельно через scenario_update_source. botId передан → бот-сценарий (source.type BOT), сразу привязан к этому боту — нужен доступ на редактирование его графов (owner/EDITOR/ADMIN), иначе 403/404; публикуется он уже через publish_graph, не через scenario_publish. Лимит сценариев тарифа — HTTP 402. Триггеры сценария (узел TRIGGER_*): для заказов владельца — TRIGGER_ORDER_CREATED («Заказ создан») и TRIGGER_ORDER_PAID («Заказ оплачен»), config {siteId?, channel?: \"site\"|\"bot\"}, переменные {{order.*}} (скилл build-bot-funnel).", inputSchema: { type: "object", properties: { name: { type: "string" }, botId: { type: "string", description: "UUID бота — делает сценарий бот-сценарием вместо вебхук-сценария" } }, required: ["name"] } },
|
|
1269
1269
|
{ name: "scenario_get", description: "Сценарий по id (GET /api/scenarios/{id}) — тот же граф, что get_graph, плюс source: {type: BOT|WEBHOOK, config: у WEBHOOK path/secret входа и (если задан) botId выхода, у BOT — только botId} и outputBinding {channel, recipientFrom}. Свой сценарий — всегда; чужой бот-сценарий — если есть доступ к боту (как GET .../graphs/{id}); чужой вебхук-сценарий — 404 (в нём секрет входа, виден только автору).", inputSchema: { type: "object", properties: { scenarioId: { type: "string" } }, required: ["scenarioId"] } },
|
|
1270
1270
|
{ name: "scenario_update_source", description: "Привязка выхода вебхук-сценария, ЦЕЛИКОМ заменяет текущую (PUT /api/scenarios/{id}/source) — только у source.type WEBHOOK, у BOT-сценария 409. botId — бот-выход (нужен доступ на редактирование его графов — owner/EDITOR/ADMIN, иначе 403/404); НЕ передан → привязка снимается, сценарий остаётся headless без бота. channel — OutputChannel: TG_BOT_API (обычный Bot API Telegram/MAX) | TG_USERBOT | IG | MAX; не передан — прежнее значение сохраняется. recipientFrom — откуда брать chatId получателя (например \"CHAT\" — чат-инициатор, или имя переменной сценария); НЕ передан → очищается в пустую строку (поле не частичное — шли его всегда, если хочешь сохранить текущее значение, сначала прочитай scenario_get). У опубликованного сценария новая привязка применяется сразу; если она добавляет НОВЫЕ ошибки проверки (платформа/лимит), которых не было при старой, — HTTP 422 со всеми code@nodeId, ничего не сохранено (уже имевшиеся ошибки не блокируют).", inputSchema: { type: "object", properties: { scenarioId: { type: "string" }, botId: { type: "string", description: "UUID бота-выхода; опусти, чтобы снять привязку" }, channel: { type: "string", enum: ["TG_BOT_API", "TG_USERBOT", "IG", "MAX"] }, recipientFrom: { type: "string", description: "Не передашь — очистится в пустую строку" } }, required: ["scenarioId"] } },
|
|
1271
1271
|
{ name: "scenario_publish", description: "Опубликовать вебхук-сценарий (POST /api/scenarios/{id}/publish) — только source.type WEBHOOK (у BOT-сценария 409, публикуй через publish_graph). Те же проверки, что у publish_graph (узлы/лимиты/платформа выходного бота, если он задан): отказ → HTTP 422 со всеми причинами построчно (code@nodeId: message), ничего не опубликовано.", inputSchema: { type: "object", properties: { scenarioId: { type: "string" } }, required: ["scenarioId"] } },
|
|
@@ -1466,8 +1466,9 @@ async function handleCall(params) {
|
|
|
1466
1466
|
return okResult(await api(`/api/bots/graphs/${a.graphId}/dry-run`, { method: "POST", body: { kind: a.kind, value, fromUsername: a.fromUsername, presetVariables: a.presetVariables, presetTags: a.presetTags } }));
|
|
1467
1467
|
}
|
|
1468
1468
|
case "publish_graph": {
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1469
|
+
const g = await invokeGuarded("graph_publish", { graphId: a.graphId, ...(a.secondary === true ? { secondary: true } : {}) }, a);
|
|
1470
|
+
if (g.pending) return g.pending;
|
|
1471
|
+
const pub = g.data;
|
|
1471
1472
|
// errors[] приходит с HTTP 200, но для пользователя это «не получилось» — и самый
|
|
1472
1473
|
// ценный сигнал: видно, какого узла/возможности ему не хватило.
|
|
1473
1474
|
if (Array.isArray(pub?.errors) && pub.errors.length) {
|
|
@@ -1524,8 +1525,11 @@ async function handleCall(params) {
|
|
|
1524
1525
|
case "copy_graph":
|
|
1525
1526
|
return okResult(await api(`/api/bots/graphs/${a.graphId}/copy`, { method: "POST", body: { targetBotId: a.targetBotId, preview: a.preview === true } }));
|
|
1526
1527
|
case "delete_graph":
|
|
1527
|
-
|
|
1528
|
+
{
|
|
1529
|
+
const g = await invokeGuarded("graph_delete", { graphId: a.graphId }, a);
|
|
1530
|
+
if (g.pending) return g.pending;
|
|
1528
1531
|
return okResult(`🗑️ Граф ${a.graphId} удалён.`);
|
|
1532
|
+
}
|
|
1529
1533
|
case "set_active_graph":
|
|
1530
1534
|
await api(`/api/bots/${a.botId}/active-graph`, { method: "POST", body: { graphId: a.graphId } });
|
|
1531
1535
|
return okResult(`✅ Активный граф бота ${a.botId} → ${a.graphId}.`);
|
|
@@ -1571,10 +1575,13 @@ async function handleCall(params) {
|
|
|
1571
1575
|
if (!hasOps && !a.init) throw new Error("Передай ops — массив операций (см. site_schema) — или init, чтобы только создать черновик.");
|
|
1572
1576
|
if (a.init != null && !SITE_INITS.includes(a.init)) throw new Error(`init — одно из: ${SITE_INITS.join(", ")}.`);
|
|
1573
1577
|
const ops = hasOps ? a.ops : SITE_NOOP_OPS; // бэкенд отвечает 422 на пустой ops[]
|
|
1574
|
-
|
|
1578
|
+
const g = await invokeGuarded("site_edit", { siteId: a.siteId, ops, revision: a.revision, init: a.init }, a);
|
|
1579
|
+
return g.pending || okResult(g.data);
|
|
1575
1580
|
}
|
|
1576
1581
|
case "site_publish": {
|
|
1577
|
-
const
|
|
1582
|
+
const g = await invokeGuarded("site_publish", { siteId: a.siteId }, a);
|
|
1583
|
+
if (g.pending) return g.pending;
|
|
1584
|
+
const r = g.data;
|
|
1578
1585
|
return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
|
|
1579
1586
|
}
|
|
1580
1587
|
case "site_upload_asset": return okResult(await uploadSiteAsset(a.siteId, { filePath: a.path, url: a.url }));
|
|
@@ -1587,7 +1594,9 @@ async function handleCall(params) {
|
|
|
1587
1594
|
}
|
|
1588
1595
|
case "site_rollback": {
|
|
1589
1596
|
if (typeof a.revision !== "number") throw new Error("Передай revision — номер публикации (site_get → versions[]).");
|
|
1590
|
-
const
|
|
1597
|
+
const g = await invokeGuarded("site_rollback", { siteId: a.siteId, revision: a.revision }, a);
|
|
1598
|
+
if (g.pending) return g.pending;
|
|
1599
|
+
const r = g.data;
|
|
1591
1600
|
return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
|
|
1592
1601
|
}
|
|
1593
1602
|
case "site_domains": {
|
|
@@ -1629,9 +1638,13 @@ async function handleCall(params) {
|
|
|
1629
1638
|
const errs = validateAgentTools(a.patch.tools);
|
|
1630
1639
|
if (errs.length) throw new Error("Инструменты агента не прошли проверку:\n- " + errs.join("\n- "));
|
|
1631
1640
|
}
|
|
1632
|
-
|
|
1641
|
+
const g = await invokeGuarded("agent_update", { agentId: a.agentId, patch: a.patch }, a);
|
|
1642
|
+
return g.pending || okResult(g.data);
|
|
1643
|
+
}
|
|
1644
|
+
case "agent_publish": {
|
|
1645
|
+
const g = await invokeGuarded("agent_publish", { agentId: a.agentId }, a);
|
|
1646
|
+
return g.pending || okResult(g.data);
|
|
1633
1647
|
}
|
|
1634
|
-
case "agent_publish": return okResult(await api(`/api/bots/agents/${a.agentId}/publish`, { method: "POST" }));
|
|
1635
1648
|
case "agent_health": return okResult(await api(`/api/bots/agents/${a.agentId}/health`));
|
|
1636
1649
|
case "agent_test_chat": {
|
|
1637
1650
|
if (!a.question || !String(a.question).trim()) throw new Error("Передай question.");
|
|
@@ -1655,7 +1668,10 @@ async function handleCall(params) {
|
|
|
1655
1668
|
}
|
|
1656
1669
|
case "kb_add_site": {
|
|
1657
1670
|
if (!a.url) throw new Error("Передай url.");
|
|
1658
|
-
|
|
1671
|
+
// schedule в реестре нет — такой вызов остаётся прямым REST
|
|
1672
|
+
if (a.schedule) return okResult(await api(`/api/bots/kb/${a.kbId}/docs/site`, { method: "POST", body: { url: a.url, schedule: a.schedule } }));
|
|
1673
|
+
const g = await invokeGuarded("kb_add_site", { kbId: a.kbId, url: a.url }, a);
|
|
1674
|
+
return g.pending || okResult(g.data);
|
|
1659
1675
|
}
|
|
1660
1676
|
case "kb_reindex": return okResult(await api(`/api/bots/kb/${a.kbId}/docs/${a.docId}/reindex`, { method: "POST", body: { headerRow: a.headerRow } }));
|
|
1661
1677
|
case "analytics_ask": {
|
|
@@ -1710,7 +1726,8 @@ async function handleCall(params) {
|
|
|
1710
1726
|
case "dialog_reply": {
|
|
1711
1727
|
const text = String(a.text ?? "").trim();
|
|
1712
1728
|
if (!text) throw new Error("Передай text — текст ответа.");
|
|
1713
|
-
|
|
1729
|
+
const g = await invokeGuarded("dialog_reply", { botId: a.botId, chatId: chatIdArg(a.chatId), text }, a);
|
|
1730
|
+
return g.pending || okResult(g.data);
|
|
1714
1731
|
}
|
|
1715
1732
|
// ---- Запись на слоты ----
|
|
1716
1733
|
case "booking_calendar_list": return okResult(await api("/api/bots/booking/calendars"));
|
|
@@ -1775,7 +1792,8 @@ async function handleCall(params) {
|
|
|
1775
1792
|
case "booking_cancel": {
|
|
1776
1793
|
const id = uuidArg(a.calendarId, "calendarId");
|
|
1777
1794
|
const bookingId = uuidArg(a.bookingId, "bookingId");
|
|
1778
|
-
await
|
|
1795
|
+
const g = await invokeGuarded("booking_cancel", { calendarId: id, bookingId }, a);
|
|
1796
|
+
if (g.pending) return g.pending;
|
|
1779
1797
|
return okResult(`✅ Бронь ${bookingId} отменена, слот свободен.`);
|
|
1780
1798
|
}
|
|
1781
1799
|
case "booking_confirm":
|
|
@@ -1870,8 +1888,8 @@ async function handleCall(params) {
|
|
|
1870
1888
|
case "scenario_runs": return okResult(await api(`/api/bots/graphs/${uuidArg(a.graphId, "graphId")}/runs${pageQs(a)}`));
|
|
1871
1889
|
case "bot_delete": {
|
|
1872
1890
|
const botId = uuidArg(a.botId, "botId");
|
|
1873
|
-
|
|
1874
|
-
|
|
1891
|
+
const g = await invokeGuarded("bot_delete", { botId }, a);
|
|
1892
|
+
if (g.pending) return g.pending;
|
|
1875
1893
|
return okResult(`🗑️ Бот ${botId} удалён вместе со сценариями и подписчиками.`);
|
|
1876
1894
|
}
|
|
1877
1895
|
// ---- Паритет платформы, часть 2 ----
|
|
@@ -2156,10 +2174,15 @@ async function handleCall(params) {
|
|
|
2156
2174
|
const inactive = a.inactive === true; // перенос из другого конструктора: не трогать живой вебхук
|
|
2157
2175
|
let bot;
|
|
2158
2176
|
try {
|
|
2159
|
-
bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
|
|
2177
|
+
if (inactive) bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
|
|
2178
|
+
else {
|
|
2179
|
+
const g = await invokeGuarded("bot_connect", { platform, token }, a);
|
|
2180
|
+
if (g.pending) return g.pending;
|
|
2181
|
+
bot = g.data;
|
|
2182
|
+
}
|
|
2160
2183
|
} catch (e) {
|
|
2161
2184
|
// 400 приходит строкой-причиной (неверный токен, бот уже подключён) — её и показываем.
|
|
2162
|
-
if (e.status === 400) throw new Error(`Бот не подключён: ${
|
|
2185
|
+
if (e.status === 400) throw new Error(`Бот не подключён: ${bodyReason(e.data) || "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
|
|
2163
2186
|
throw e;
|
|
2164
2187
|
}
|
|
2165
2188
|
if (a.name && bot?.id) {
|
|
@@ -2181,7 +2204,10 @@ async function handleCall(params) {
|
|
|
2181
2204
|
case "vk_landing_delete":
|
|
2182
2205
|
await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`, { method: "DELETE" });
|
|
2183
2206
|
return okResult({ deleted: true });
|
|
2184
|
-
case "bot_stop":
|
|
2207
|
+
case "bot_stop": {
|
|
2208
|
+
const g = await invokeGuarded("bot_stop", { botId: a.botId }, a);
|
|
2209
|
+
return g.pending || okResult(g.data);
|
|
2210
|
+
}
|
|
2185
2211
|
case "bot_resume": return okResult(await api(`/api/bots/${a.botId}/resume`, { method: "POST" }));
|
|
2186
2212
|
// ---- Подключения ----
|
|
2187
2213
|
case "connect_integration": {
|
|
@@ -2341,7 +2367,9 @@ async function handleCall(params) {
|
|
|
2341
2367
|
if (a.scheduledAt !== undefined) throw new Error(`scheduledAt ${body.scheduledAt} уже прошло — укажи будущее время или не передавай его (отправить сейчас).`);
|
|
2342
2368
|
body.scheduledAt = null;
|
|
2343
2369
|
}
|
|
2344
|
-
const
|
|
2370
|
+
const g = await invokeGuarded("broadcast_send_now", body, a);
|
|
2371
|
+
if (g.pending) return g.pending;
|
|
2372
|
+
const r = g.data;
|
|
2345
2373
|
let draftDeleted = false;
|
|
2346
2374
|
if (a.draftId) { try { await api(`/api/bots/broadcasts/drafts/${a.draftId}`, { method: "DELETE" }); draftDeleted = true; } catch { /* не критично */ } }
|
|
2347
2375
|
// dedup/stagger → бэкенд создаёт BroadcastGroup вместо прямых TgBroadcast (другая форма ответа: id/perBot[]/status вместо broadcastIds/totalAudience).
|
|
@@ -2471,9 +2499,11 @@ async function handleCall(params) {
|
|
|
2471
2499
|
}
|
|
2472
2500
|
return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/ops/preview`, { method: "POST", body }));
|
|
2473
2501
|
}
|
|
2474
|
-
case "broadcast_cancel":
|
|
2475
|
-
await
|
|
2502
|
+
case "broadcast_cancel": {
|
|
2503
|
+
const g = await invokeGuarded("broadcast_cancel", { broadcastId: a.broadcastId }, a);
|
|
2504
|
+
if (g.pending) return g.pending;
|
|
2476
2505
|
return okResult(`⏹️ Рассылка ${a.broadcastId} отменяется (CANCELLING → CANCELLED).`);
|
|
2506
|
+
}
|
|
2477
2507
|
case "broadcast_recurring": {
|
|
2478
2508
|
const base = "/api/bots/broadcasts/recurring";
|
|
2479
2509
|
if (a.action === "list") return okResult(await api(base));
|
|
@@ -2584,7 +2614,10 @@ async function handleCall(params) {
|
|
|
2584
2614
|
await api(`/api/bots/chains/${chainId}`, { method: "DELETE" });
|
|
2585
2615
|
return okResult(`🗑️ Цепочка ${chainId} удалена.`);
|
|
2586
2616
|
}
|
|
2587
|
-
case "chain_enable":
|
|
2617
|
+
case "chain_enable": {
|
|
2618
|
+
const g = await invokeGuarded("chain_enable", { chainId: uuidArg(a.chainId, "chainId") }, a);
|
|
2619
|
+
return g.pending || okResult(g.data);
|
|
2620
|
+
}
|
|
2588
2621
|
case "chain_pause": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/pause`, { method: "POST" }));
|
|
2589
2622
|
case "chain_runs": {
|
|
2590
2623
|
const chainId = uuidArg(a.chainId, "chainId");
|
|
@@ -2780,6 +2813,32 @@ async function callCatalog(params) {
|
|
|
2780
2813
|
}
|
|
2781
2814
|
}
|
|
2782
2815
|
|
|
2816
|
+
/**
|
|
2817
|
+
* Опасное действие через единый реестр: сервер сам требует подтверждение (409 + confirmToken) и пишет вызов в журнал.
|
|
2818
|
+
* { pending } — готовый MCP-результат «спроси пользователя» (с описанием изменений и токеном), { data } — выполнено.
|
|
2819
|
+
* Повтор: те же аргументы + confirmToken (параметр самого инструмента).
|
|
2820
|
+
*/
|
|
2821
|
+
async function invokeGuarded(name, args, a) {
|
|
2822
|
+
const clean = Object.fromEntries(Object.entries(args).filter(([, v]) => v !== undefined));
|
|
2823
|
+
if (typeof a?.confirmToken === "string" && a.confirmToken) clean.confirmToken = a.confirmToken;
|
|
2824
|
+
try {
|
|
2825
|
+
return { data: await api("/api/agent-tools/invoke", { method: "POST", body: { name, args: clean } }) };
|
|
2826
|
+
} catch (e) {
|
|
2827
|
+
const text = e?.status === 409 ? confirmationText(e.data) : null;
|
|
2828
|
+
if (text) return { pending: okResult(text) };
|
|
2829
|
+
throw e;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
/** Обратимые правки: сервер обычно не требует подтверждения, токен нужен лишь при явном 409. */
|
|
2833
|
+
const SOFT_CONFIRM_TOOLS = new Set(["site_edit", "agent_update", "kb_add_site", "chain_enable"]);
|
|
2834
|
+
const GUARDED_TOOLS = new Set(["publish_graph", "site_publish", "site_rollback", "agent_publish", "booking_cancel", "bot_delete", "broadcast_cancel", "broadcast_send",
|
|
2835
|
+
"delete_graph", "agent_update", "chain_enable", "kb_add_site", "create_bot", "bot_stop", "dialog_reply", "site_edit"]);
|
|
2836
|
+
for (const t of TOOLS) {
|
|
2837
|
+
if (!GUARDED_TOOLS.has(t.name)) continue;
|
|
2838
|
+
const soft = SOFT_CONFIRM_TOOLS.has(t.name);
|
|
2839
|
+
t.inputSchema.properties = { ...(t.inputSchema.properties || {}), confirmToken: { type: "string", description: soft ? "Только если сервер вернул запрос подтверждения (409): токен одноразовый, при новом 409 используй новый токен и покажи пользователю новое описание изменений. Обычно не нужен." : "Токен подтверждения из ответа «Нужно подтверждение пользователя»: повтори вызов с теми же аргументами после явного согласия человека. Без него сервер ничего не выполнит." } };
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2783
2842
|
function notifyListChanged() { send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }); }
|
|
2784
2843
|
/** Обновить каталог в фоне; изменился набор — сказать клиенту перечитать tools/list. */
|
|
2785
2844
|
function refreshCatalog() {
|