@retensy/mcp 0.50.0 → 0.52.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 +2 -2
- package/package.json +2 -2
- package/src/catalog.mjs +15 -2
- package/src/index.mjs +89 -26
package/README.md
CHANGED
|
@@ -224,8 +224,8 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
224
224
|
| `site_list()` | сайты пользователя (id, mode, url, publishedRevision, status — `BLOCKED` у заблокированных модерацией, blockedReason) |
|
|
225
225
|
| `site_create(title, slug?, template?)` | новый сайт из блоков → `id`; `template` (`starter`/`blank`/`mini-landing`/`shop-starter`) — сразу черновик из шаблона; число сайтов ограничено тарифом — сверх лимита HTTP 402 с причиной и ссылкой на смену тарифа |
|
|
226
226
|
| `site_get(siteId, saveToFile?)` | модель сайта (`revision`, `draft`, `versions[]` публикаций) |
|
|
227
|
-
| `site_schema()` |
|
|
228
|
-
| `site_edit(siteId, ops[]?, revision?, init?)` | правка операциями (`init`: `starter`/`blank`/`mini-landing`/`shop-starter`; только `init` — черновик из
|
|
227
|
+
| `site_schema(full?)` | компактный справочник модели и операций (~10 КБ); `full:true` — полная JSON Schema — читать перед правкой |
|
|
228
|
+
| `site_edit(siteId, ops[]?, revision?, init?, reset?)` | правка операциями (`init`: `starter`/`blank`/`mini-landing`/`shop-starter`; только `init` — черновик из шаблона; `reset:true` с `init` и `revision` — пересобрать черновик из шаблона, деструктивно), всё или ничего: страницы и папки, блоки, Zero-элементы, код блока (`get/set/add_block_code`), дизайны и их кадры, шаблоны (`add_template`), тема, попапы; пиксели аналитики (GA4/GTM/Meta/VK/Яндекс.Метрика) — тоже сюда, `set_settings {analytics: {...}}` (отдельного инструмента нет — те же `settings.analytics` модели сайта, валидируются форматом id на сервере) |
|
|
229
229
|
| `site_templates(category?, full?)` | библиотека шаблонов блоков для `add_template` |
|
|
230
230
|
| `site_publish(siteId)` | опубликовать черновик → `url` |
|
|
231
231
|
| `site_rollback(siteId, revision)` | вернуть прошлую публикацию |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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": {
|
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
|
@@ -1079,7 +1079,7 @@ const TOOLS = [
|
|
|
1079
1079
|
{ name: "site_list", description: "Сайты (раздел «Страницы», GET /api/bots/pages): id, title, mode (BLOCKS — сайт из блоков, CODE — файлы/Mini App), url (основной адрес), publishedRevision, status (ACTIVE | BLOCKED — заблокирован модерацией: посетители видят HTTP 451, удалить такой сайт нельзя — 409) и blockedReason. Включает и свои сайты, и сайты команды проекта (привязанные к боту, где тебя пригласили) — у каждого accessRole (OWNER/ADMIN/EDITOR/VIEWER), у чужих — ownerName. Доступ проверяется на каждый вызов. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1080
1080
|
{ name: "site_create", description: "Создать сайт из блоков (POST /api/bots/pages, mode=BLOCKS). Возвращает id. Дальше: site_edit (init=starter — стартовый лендинг, init=blank — пустая главная, init=mini-landing — мини-лендинг с кнопками мессенджеров, init=shop-starter — многостраничный магазин: главная, каталог, о нас, доставка, контакты, политика конфиденциальности, оферта, корзина; общие шапка и подвал) → site_publish. template (starter|blank|mini-landing|shop-starter) — сразу создать черновик из шаблона (как «Мини-лендинг» в редакторе), в ответе revision. slug — «название» в адресе pages.retensy.com/<id>/<slug>/ (необязательно, по умолчанию транслит title). Число сайтов ограничено тарифом (free — 1, «Старт» — 3, «Про» — 10, с «Бизнеса» — без лимита): сверх лимита — HTTP 402 с причиной и upgradeUrl (смена тарифа только в браузере, передай ссылку пользователю). Заблокированный модерацией сайт в site_list имеет status=BLOCKED и blockedReason, посетители видят HTTP 451.", inputSchema: { type: "object", properties: { title: { type: "string" }, slug: { type: "string" }, template: { type: "string", enum: ["starter", "blank", "mini-landing", "shop-starter"] } }, required: ["title"] } },
|
|
1081
1081
|
{ name: "site_get", description: "Модель сайта из блоков (GET /api/bots/pages/{siteId}/document): revision, draft (SiteModel: theme, globals.header/footer, pages[].blocks[], popups[]) — id страниц/блоков/попапов нужны для site_edit. draft=null — сайт пуст (первый site_edit создаст его). saveToFile — записать модель на диск и вернуть путь.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, saveToFile: { type: "string" } }, required: ["siteId"] } },
|
|
1082
|
-
{ name: "site_schema", description: "
|
|
1082
|
+
{ name: "site_schema", description: "Компактный справочник модели сайта и операций правки (ops) — какие блоки и поля бывают (GET /api/bots/pages/schema, ~10 КБ). Читай перед первой правкой. full:true — полная JSON Schema (GET ?full=true), большая.", inputSchema: { type: "object", properties: { full: { type: "boolean", description: "true = полная JSON Schema вместо компактного справочника" } } } },
|
|
1083
1083
|
{ name: "site_edit", description: "Правка сайта операциями — всё или ничего (POST /api/bots/pages/{siteId}/document/ops). " +
|
|
1084
1084
|
"Страницы: add_page{title} · update_page{pageId,patch: {title?, path?, seo?{title,description,noindex,ogTitle,ogDescription,ogImage}, showHeader?, showFooter?, folder?}} · remove_page{pageId} · move_page{pageId,delta}. " +
|
|
1085
1085
|
"Папки страниц: add_folder{name} (id в results) · rename_folder{folderId,name} · remove_folder{folderId}; страница в папку — update_page{pageId, patch:{folder: folderId}}. " +
|
|
@@ -1091,8 +1091,8 @@ const TOOLS = [
|
|
|
1091
1091
|
"Zero-kinds form/product-list/product: form — props.form как у блока form (fields, submitText); product-list — props: categoryId?, limit, columns (значения по экранам {d, t?, m?}); product — productId обязателен (товар из каталога). Новые Zero-блоки создаются с артбордами 1440/768/390 (d/t/m). " +
|
|
1092
1092
|
"Сайт: set_global{slot: header|footer, on} · set_theme{theme} · set_settings{settings} · add_popup{name} · update_popup{popupId,name?,width?} · remove_popup{popupId}. " +
|
|
1093
1093
|
"props/style/theme/frame — JSON Merge Patch (null удаляет ключ, массивы заменяются целиком). Типы блоков: header, cover, text, image, gallery, buttons, features, form, video, html, spacer, footer, zero, messengers. " +
|
|
1094
|
-
"Значения по экранам: {d, t?, m?} (десктоп/планшет/телефон). revision — защита от перезаписи (409, если сайт изменили); init (starter|blank|mini-landing|shop-starter) — с чего начать пустой сайт (на сайте с черновиком игнорируется); shop-starter — многостраничный магазин (главная, каталог, о нас, доставка, контакты, политика, оферта, корзина; общие шапка и подвал); только init без ops — создать черновик из шаблона. " +
|
|
1095
|
-
"Ответ: новая revision и results[] с id созданного (и code у get_block_code). Ошибки — HTTP 422 с путями. Тариф: HTML-блок и HTML-элемент Zero публикуются только на платном тарифе (422 при site_publish).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, ops: { type: "array", items: { type: "object" } }, revision: { type: "number" }, init: { type: "string", enum: ["starter", "blank", "mini-landing", "shop-starter"] } }, required: ["siteId"] } },
|
|
1094
|
+
"Значения по экранам: {d, t?, m?} (десктоп/планшет/телефон). revision — защита от перезаписи (409, если сайт изменили); init (starter|blank|mini-landing|shop-starter) — с чего начать пустой сайт (на сайте с черновиком игнорируется); shop-starter — многостраничный магазин (главная, каталог, о нас, доставка, контакты, политика, оферта, корзина; общие шапка и подвал); только init без ops — создать черновик из шаблона. reset:true (с init и revision) — пересобрать существующий черновик из шаблона init, ДЕСТРУКТИВНО (текущий черновик заменяется). " +
|
|
1095
|
+
"Ответ: новая revision и results[] с id созданного (и code у get_block_code). Ошибки — HTTP 422 с путями. Тариф: HTML-блок и HTML-элемент Zero публикуются только на платном тарифе (422 при site_publish).", inputSchema: { type: "object", properties: { siteId: { type: "string" }, ops: { type: "array", items: { type: "object" } }, revision: { type: "number" }, init: { type: "string", enum: ["starter", "blank", "mini-landing", "shop-starter"] }, reset: { type: "boolean", description: "true = пересобрать существующий черновик из шаблона init (ДЕСТРУКТИВНО: текущий черновик заменяется). Требует init и revision." } }, required: ["siteId"] } },
|
|
1096
1096
|
{ name: "site_publish", description: "Опубликовать черновик сайта (POST /api/bots/pages/{siteId}/publish): рендер в статику, адрес начинает отдавать новую версию. Ошибки проверки — HTTP 422 с путями. Возвращает publishedRevision и url.", inputSchema: { type: "object", properties: { siteId: { type: "string" } }, required: ["siteId"] } },
|
|
1097
1097
|
{ name: "site_upload_asset", description: "Загрузить картинку/видео в сайт (POST /api/bots/pages/{siteId}/upload, папка assets). Передай path (локальный файл) ИЛИ url. Возвращает asset — строку вида assets/<имя> для полей image/logo/icon/style.bg.image.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, path: { type: "string" }, url: { type: "string" } }, required: ["siteId"] } },
|
|
1098
1098
|
{ name: "site_rollback", description: "Вернуть прошлую публикацию сайта (POST /api/bots/pages/{siteId}/publish/rollback): revision — номер из истории публикаций (site_get → versions[]). Черновик заменяется этой версией и сразу публикуется. Возвращает publishedRevision и url.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, revision: { type: "number" } }, required: ["siteId", "revision"] } },
|
|
@@ -1196,7 +1196,7 @@ const TOOLS = [
|
|
|
1196
1196
|
{ name: "booking_cancel", description: "Отменить бронь и освободить слот (POST /api/bots/booking/calendars/{calendarId}/bookings/{bookingId}/cancel). Повторная отмена — без ошибки. bookingId — из booking_list.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
|
|
1197
1197
|
{ name: "booking_confirm", description: "Подтвердить бронь, ждущую решения владельца (MANUAL confirmationMode) — POST .../bookings/{bookingId}/confirm. Клиенту уходит уведомление, если бронь пришла из чата бота. HTTP 404 — бронь не этого календаря, 409 — уже не ждёт решения (подтверждена/отменена/истёк pendingHoldHours).", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
|
|
1198
1198
|
{ name: "booking_reject", description: "Отклонить бронь, ждущую решения владельца (MANUAL confirmationMode) — POST .../bookings/{bookingId}/reject: освобождает слот, клиенту уходит «отклонено владельцем» (в отличие от booking_cancel — то работает на любой активной брони и ничего не присылает клиенту). HTTP 404/409 — как у booking_confirm.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, bookingId: { type: "string" } }, required: ["calendarId", "bookingId"] } },
|
|
1199
|
-
{ name: "site_templates", description: "Библиотека шаблонов блоков сайта (GET /api/bots/pages/templates): {categories: [{id, title, description?}], templates: [{id, category, title, description?, blocks: сколько блоков вставится}]}. Вставка — site_edit add_template {container, templateId, after?} (results.id — первый блок, results.ids — все); дальше блоки правятся как обычные. category — фильтр по id категории.", inputSchema: { type: "object", properties: { category: { type: "string" } } } },
|
|
1199
|
+
{ name: "site_templates", description: "Библиотека шаблонов блоков сайта (GET /api/bots/pages/templates): {categories: [{id, title, description?}], templates: [{id, category, title, description?, blocks: сколько блоков вставится}]}. Вставка — site_edit add_template {container, templateId, after?} (results.id — первый блок, results.ids — все); дальше блоки правятся как обычные. category — фильтр по id категории. Ответ также содержит siteTemplates — стартовые шаблоны сайта (starter, blank, mini-landing, shop-starter) с описаниями: для site_create template / site_edit init.", inputSchema: { type: "object", properties: { category: { type: "string" } } } },
|
|
1200
1200
|
// ---- Паритет платформы, часть 2: бот, база, агент, заявки, подписчики, каналы, ссылки, аналитика ----
|
|
1201
1201
|
{ name: "bot_rename", description: "Переименовать бота (PATCH /api/bots/{botId} {name}): отображаемое имя в кабинете, до 250 символов. Пустое name сбрасывает имя на @username. Имя в самом Telegram/MAX не меняется.", inputSchema: { type: "object", properties: { botId: { type: "string" }, name: { type: "string" } }, required: ["botId", "name"] } },
|
|
1202
1202
|
{ name: "bot_change_token", description: "Сменить токен бота (POST /api/bots/{botId}/token {token}): сервер проверяет токен и перезапускает приём сообщений; сценарии, подписчики и ссылки остаются. Нужен, если токен перевыпущен в @BotFather (/revoke) или MAX. Неверный или чужой токен → ошибка с причиной. Владелец или ADMIN.", inputSchema: { type: "object", properties: { botId: { type: "string" }, token: { type: "string", description: "Новый токен бота" } }, required: ["botId", "token"] } },
|
|
@@ -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}.`);
|
|
@@ -1565,16 +1569,23 @@ async function handleCall(params) {
|
|
|
1565
1569
|
}
|
|
1566
1570
|
return okResult(doc);
|
|
1567
1571
|
}
|
|
1568
|
-
case "site_schema": return okResult(await api("/api/bots/pages/schema"));
|
|
1572
|
+
case "site_schema": return okResult(await api(a.full === true ? "/api/bots/pages/schema?full=true" : "/api/bots/pages/schema"));
|
|
1569
1573
|
case "site_edit": {
|
|
1570
1574
|
const hasOps = Array.isArray(a.ops) && a.ops.length > 0;
|
|
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(", ")}.`);
|
|
1577
|
+
if (a.reset === true) {
|
|
1578
|
+
if (!a.init) throw new Error("reset — пересборка черновика из шаблона: нужен init (starter|blank|mini-landing|shop-starter).");
|
|
1579
|
+
if (typeof a.revision !== "number") throw new Error("reset удаляет текущий черновик — передай revision (site_get → revision).");
|
|
1580
|
+
}
|
|
1573
1581
|
const ops = hasOps ? a.ops : SITE_NOOP_OPS; // бэкенд отвечает 422 на пустой ops[]
|
|
1574
|
-
|
|
1582
|
+
const g = await invokeGuarded("site_edit", { siteId: a.siteId, ops, revision: a.revision, init: a.init, reset: a.reset === true ? true : undefined }, a);
|
|
1583
|
+
return g.pending || okResult(g.data);
|
|
1575
1584
|
}
|
|
1576
1585
|
case "site_publish": {
|
|
1577
|
-
const
|
|
1586
|
+
const g = await invokeGuarded("site_publish", { siteId: a.siteId }, a);
|
|
1587
|
+
if (g.pending) return g.pending;
|
|
1588
|
+
const r = g.data;
|
|
1578
1589
|
return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
|
|
1579
1590
|
}
|
|
1580
1591
|
case "site_upload_asset": return okResult(await uploadSiteAsset(a.siteId, { filePath: a.path, url: a.url }));
|
|
@@ -1587,7 +1598,9 @@ async function handleCall(params) {
|
|
|
1587
1598
|
}
|
|
1588
1599
|
case "site_rollback": {
|
|
1589
1600
|
if (typeof a.revision !== "number") throw new Error("Передай revision — номер публикации (site_get → versions[]).");
|
|
1590
|
-
const
|
|
1601
|
+
const g = await invokeGuarded("site_rollback", { siteId: a.siteId, revision: a.revision }, a);
|
|
1602
|
+
if (g.pending) return g.pending;
|
|
1603
|
+
const r = g.data;
|
|
1591
1604
|
return okResult({ publishedRevision: r?.publishedRevision, url: r?.page?.url });
|
|
1592
1605
|
}
|
|
1593
1606
|
case "site_domains": {
|
|
@@ -1629,9 +1642,13 @@ async function handleCall(params) {
|
|
|
1629
1642
|
const errs = validateAgentTools(a.patch.tools);
|
|
1630
1643
|
if (errs.length) throw new Error("Инструменты агента не прошли проверку:\n- " + errs.join("\n- "));
|
|
1631
1644
|
}
|
|
1632
|
-
|
|
1645
|
+
const g = await invokeGuarded("agent_update", { agentId: a.agentId, patch: a.patch }, a);
|
|
1646
|
+
return g.pending || okResult(g.data);
|
|
1647
|
+
}
|
|
1648
|
+
case "agent_publish": {
|
|
1649
|
+
const g = await invokeGuarded("agent_publish", { agentId: a.agentId }, a);
|
|
1650
|
+
return g.pending || okResult(g.data);
|
|
1633
1651
|
}
|
|
1634
|
-
case "agent_publish": return okResult(await api(`/api/bots/agents/${a.agentId}/publish`, { method: "POST" }));
|
|
1635
1652
|
case "agent_health": return okResult(await api(`/api/bots/agents/${a.agentId}/health`));
|
|
1636
1653
|
case "agent_test_chat": {
|
|
1637
1654
|
if (!a.question || !String(a.question).trim()) throw new Error("Передай question.");
|
|
@@ -1655,7 +1672,10 @@ async function handleCall(params) {
|
|
|
1655
1672
|
}
|
|
1656
1673
|
case "kb_add_site": {
|
|
1657
1674
|
if (!a.url) throw new Error("Передай url.");
|
|
1658
|
-
|
|
1675
|
+
// schedule в реестре нет — такой вызов остаётся прямым REST
|
|
1676
|
+
if (a.schedule) return okResult(await api(`/api/bots/kb/${a.kbId}/docs/site`, { method: "POST", body: { url: a.url, schedule: a.schedule } }));
|
|
1677
|
+
const g = await invokeGuarded("kb_add_site", { kbId: a.kbId, url: a.url }, a);
|
|
1678
|
+
return g.pending || okResult(g.data);
|
|
1659
1679
|
}
|
|
1660
1680
|
case "kb_reindex": return okResult(await api(`/api/bots/kb/${a.kbId}/docs/${a.docId}/reindex`, { method: "POST", body: { headerRow: a.headerRow } }));
|
|
1661
1681
|
case "analytics_ask": {
|
|
@@ -1710,7 +1730,8 @@ async function handleCall(params) {
|
|
|
1710
1730
|
case "dialog_reply": {
|
|
1711
1731
|
const text = String(a.text ?? "").trim();
|
|
1712
1732
|
if (!text) throw new Error("Передай text — текст ответа.");
|
|
1713
|
-
|
|
1733
|
+
const g = await invokeGuarded("dialog_reply", { botId: a.botId, chatId: chatIdArg(a.chatId), text }, a);
|
|
1734
|
+
return g.pending || okResult(g.data);
|
|
1714
1735
|
}
|
|
1715
1736
|
// ---- Запись на слоты ----
|
|
1716
1737
|
case "booking_calendar_list": return okResult(await api("/api/bots/booking/calendars"));
|
|
@@ -1775,7 +1796,8 @@ async function handleCall(params) {
|
|
|
1775
1796
|
case "booking_cancel": {
|
|
1776
1797
|
const id = uuidArg(a.calendarId, "calendarId");
|
|
1777
1798
|
const bookingId = uuidArg(a.bookingId, "bookingId");
|
|
1778
|
-
await
|
|
1799
|
+
const g = await invokeGuarded("booking_cancel", { calendarId: id, bookingId }, a);
|
|
1800
|
+
if (g.pending) return g.pending;
|
|
1779
1801
|
return okResult(`✅ Бронь ${bookingId} отменена, слот свободен.`);
|
|
1780
1802
|
}
|
|
1781
1803
|
case "booking_confirm":
|
|
@@ -1870,8 +1892,8 @@ async function handleCall(params) {
|
|
|
1870
1892
|
case "scenario_runs": return okResult(await api(`/api/bots/graphs/${uuidArg(a.graphId, "graphId")}/runs${pageQs(a)}`));
|
|
1871
1893
|
case "bot_delete": {
|
|
1872
1894
|
const botId = uuidArg(a.botId, "botId");
|
|
1873
|
-
|
|
1874
|
-
|
|
1895
|
+
const g = await invokeGuarded("bot_delete", { botId }, a);
|
|
1896
|
+
if (g.pending) return g.pending;
|
|
1875
1897
|
return okResult(`🗑️ Бот ${botId} удалён вместе со сценариями и подписчиками.`);
|
|
1876
1898
|
}
|
|
1877
1899
|
// ---- Паритет платформы, часть 2 ----
|
|
@@ -2156,10 +2178,15 @@ async function handleCall(params) {
|
|
|
2156
2178
|
const inactive = a.inactive === true; // перенос из другого конструктора: не трогать живой вебхук
|
|
2157
2179
|
let bot;
|
|
2158
2180
|
try {
|
|
2159
|
-
bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
|
|
2181
|
+
if (inactive) bot = await api("/api/bots", { method: "POST", body: { token, platform, inactive } });
|
|
2182
|
+
else {
|
|
2183
|
+
const g = await invokeGuarded("bot_connect", { platform, token }, a);
|
|
2184
|
+
if (g.pending) return g.pending;
|
|
2185
|
+
bot = g.data;
|
|
2186
|
+
}
|
|
2160
2187
|
} catch (e) {
|
|
2161
2188
|
// 400 приходит строкой-причиной (неверный токен, бот уже подключён) — её и показываем.
|
|
2162
|
-
if (e.status === 400) throw new Error(`Бот не подключён: ${
|
|
2189
|
+
if (e.status === 400) throw new Error(`Бот не подключён: ${bodyReason(e.data) || "токен не принят"}. Проверь токен (${platform === "MAX" ? "MasterBot в MAX" : "@BotFather → /token"}).`);
|
|
2163
2190
|
throw e;
|
|
2164
2191
|
}
|
|
2165
2192
|
if (a.name && bot?.id) {
|
|
@@ -2181,7 +2208,10 @@ async function handleCall(params) {
|
|
|
2181
2208
|
case "vk_landing_delete":
|
|
2182
2209
|
await api(`/api/growth/vk-landings/${uuidArg(a.id, "id")}`, { method: "DELETE" });
|
|
2183
2210
|
return okResult({ deleted: true });
|
|
2184
|
-
case "bot_stop":
|
|
2211
|
+
case "bot_stop": {
|
|
2212
|
+
const g = await invokeGuarded("bot_stop", { botId: a.botId }, a);
|
|
2213
|
+
return g.pending || okResult(g.data);
|
|
2214
|
+
}
|
|
2185
2215
|
case "bot_resume": return okResult(await api(`/api/bots/${a.botId}/resume`, { method: "POST" }));
|
|
2186
2216
|
// ---- Подключения ----
|
|
2187
2217
|
case "connect_integration": {
|
|
@@ -2341,7 +2371,9 @@ async function handleCall(params) {
|
|
|
2341
2371
|
if (a.scheduledAt !== undefined) throw new Error(`scheduledAt ${body.scheduledAt} уже прошло — укажи будущее время или не передавай его (отправить сейчас).`);
|
|
2342
2372
|
body.scheduledAt = null;
|
|
2343
2373
|
}
|
|
2344
|
-
const
|
|
2374
|
+
const g = await invokeGuarded("broadcast_send_now", body, a);
|
|
2375
|
+
if (g.pending) return g.pending;
|
|
2376
|
+
const r = g.data;
|
|
2345
2377
|
let draftDeleted = false;
|
|
2346
2378
|
if (a.draftId) { try { await api(`/api/bots/broadcasts/drafts/${a.draftId}`, { method: "DELETE" }); draftDeleted = true; } catch { /* не критично */ } }
|
|
2347
2379
|
// dedup/stagger → бэкенд создаёт BroadcastGroup вместо прямых TgBroadcast (другая форма ответа: id/perBot[]/status вместо broadcastIds/totalAudience).
|
|
@@ -2471,9 +2503,11 @@ async function handleCall(params) {
|
|
|
2471
2503
|
}
|
|
2472
2504
|
return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/ops/preview`, { method: "POST", body }));
|
|
2473
2505
|
}
|
|
2474
|
-
case "broadcast_cancel":
|
|
2475
|
-
await
|
|
2506
|
+
case "broadcast_cancel": {
|
|
2507
|
+
const g = await invokeGuarded("broadcast_cancel", { broadcastId: a.broadcastId }, a);
|
|
2508
|
+
if (g.pending) return g.pending;
|
|
2476
2509
|
return okResult(`⏹️ Рассылка ${a.broadcastId} отменяется (CANCELLING → CANCELLED).`);
|
|
2510
|
+
}
|
|
2477
2511
|
case "broadcast_recurring": {
|
|
2478
2512
|
const base = "/api/bots/broadcasts/recurring";
|
|
2479
2513
|
if (a.action === "list") return okResult(await api(base));
|
|
@@ -2559,7 +2593,7 @@ async function handleCall(params) {
|
|
|
2559
2593
|
const r = await api("/api/bots/pages/templates");
|
|
2560
2594
|
let templates = Array.isArray(r?.templates) ? r.templates : [];
|
|
2561
2595
|
if (a.category) templates = templates.filter((t) => t?.category === a.category);
|
|
2562
|
-
return okResult({ categories: r?.categories ?? [], templates });
|
|
2596
|
+
return okResult({ categories: r?.categories ?? [], templates, siteTemplates: r?.siteTemplates ?? [] });
|
|
2563
2597
|
}
|
|
2564
2598
|
// ---- Публикации: цепочки ----
|
|
2565
2599
|
case "chain_list": return okResult(await api("/api/bots/chains"));
|
|
@@ -2584,7 +2618,10 @@ async function handleCall(params) {
|
|
|
2584
2618
|
await api(`/api/bots/chains/${chainId}`, { method: "DELETE" });
|
|
2585
2619
|
return okResult(`🗑️ Цепочка ${chainId} удалена.`);
|
|
2586
2620
|
}
|
|
2587
|
-
case "chain_enable":
|
|
2621
|
+
case "chain_enable": {
|
|
2622
|
+
const g = await invokeGuarded("chain_enable", { chainId: uuidArg(a.chainId, "chainId") }, a);
|
|
2623
|
+
return g.pending || okResult(g.data);
|
|
2624
|
+
}
|
|
2588
2625
|
case "chain_pause": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/pause`, { method: "POST" }));
|
|
2589
2626
|
case "chain_runs": {
|
|
2590
2627
|
const chainId = uuidArg(a.chainId, "chainId");
|
|
@@ -2780,6 +2817,32 @@ async function callCatalog(params) {
|
|
|
2780
2817
|
}
|
|
2781
2818
|
}
|
|
2782
2819
|
|
|
2820
|
+
/**
|
|
2821
|
+
* Опасное действие через единый реестр: сервер сам требует подтверждение (409 + confirmToken) и пишет вызов в журнал.
|
|
2822
|
+
* { pending } — готовый MCP-результат «спроси пользователя» (с описанием изменений и токеном), { data } — выполнено.
|
|
2823
|
+
* Повтор: те же аргументы + confirmToken (параметр самого инструмента).
|
|
2824
|
+
*/
|
|
2825
|
+
async function invokeGuarded(name, args, a) {
|
|
2826
|
+
const clean = Object.fromEntries(Object.entries(args).filter(([, v]) => v !== undefined));
|
|
2827
|
+
if (typeof a?.confirmToken === "string" && a.confirmToken) clean.confirmToken = a.confirmToken;
|
|
2828
|
+
try {
|
|
2829
|
+
return { data: await api("/api/agent-tools/invoke", { method: "POST", body: { name, args: clean } }) };
|
|
2830
|
+
} catch (e) {
|
|
2831
|
+
const text = e?.status === 409 ? confirmationText(e.data) : null;
|
|
2832
|
+
if (text) return { pending: okResult(text) };
|
|
2833
|
+
throw e;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
/** Обратимые правки: сервер обычно не требует подтверждения, токен нужен лишь при явном 409. */
|
|
2837
|
+
const SOFT_CONFIRM_TOOLS = new Set(["site_edit", "agent_update", "kb_add_site", "chain_enable"]);
|
|
2838
|
+
const GUARDED_TOOLS = new Set(["publish_graph", "site_publish", "site_rollback", "agent_publish", "booking_cancel", "bot_delete", "broadcast_cancel", "broadcast_send",
|
|
2839
|
+
"delete_graph", "agent_update", "chain_enable", "kb_add_site", "create_bot", "bot_stop", "dialog_reply", "site_edit"]);
|
|
2840
|
+
for (const t of TOOLS) {
|
|
2841
|
+
if (!GUARDED_TOOLS.has(t.name)) continue;
|
|
2842
|
+
const soft = SOFT_CONFIRM_TOOLS.has(t.name);
|
|
2843
|
+
t.inputSchema.properties = { ...(t.inputSchema.properties || {}), confirmToken: { type: "string", description: soft ? "Только если сервер вернул запрос подтверждения (409): токен одноразовый, при новом 409 используй новый токен и покажи пользователю новое описание изменений. Обычно не нужен." : "Токен подтверждения из ответа «Нужно подтверждение пользователя»: повтори вызов с теми же аргументами после явного согласия человека. Без него сервер ничего не выполнит." } };
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2783
2846
|
function notifyListChanged() { send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }); }
|
|
2784
2847
|
/** Обновить каталог в фоне; изменился набор — сказать клиенту перечитать tools/list. */
|
|
2785
2848
|
function refreshCatalog() {
|