@retensy/mcp 0.48.0 → 0.50.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
|
@@ -343,6 +343,14 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
343
343
|
| `ai_packs()` | каталог пакетов ответов ИИ сверх тарифа + текущий баланс (`aiPackAnswersLeft`) |
|
|
344
344
|
| `ai_pack_purchase(sku, method?, confirm:true)` | купить пакет — возвращает ссылку на оплату, деньги не списываются этим вызовом |
|
|
345
345
|
|
|
346
|
+
### Магазин (каталог)
|
|
347
|
+
|
|
348
|
+
| Tool | Назначение |
|
|
349
|
+
|---|---|
|
|
350
|
+
| `catalog_item_upsert(id?, sku?, name?, price?, images?, categoryIds?, categoryNames?, confirm:true)` | создать/обновить товар по `id` или `sku`; `images` — до 10 https-ссылок на фото; `categoryNames` — находит категории по имени или создаёт их |
|
|
351
|
+
| `catalog_category_list()` | категории магазина: `id`, `name`, `parentId` |
|
|
352
|
+
| `catalog_category_upsert(name, id?, parentId?, confirm:true)` | создать (без `id`) или переименовать (с `id`) категорию; без `parentId` при обновлении родитель сохраняется |
|
|
353
|
+
|
|
346
354
|
### Рассылки
|
|
347
355
|
|
|
348
356
|
Сообщение рассылки — как в мастере кабинета: `{type, text?, mediaUrl?, mediaUrls?, buttons?}`, до 5 сообщений.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.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": {
|
|
@@ -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/index.mjs
CHANGED
|
@@ -1044,6 +1044,9 @@ const TOOLS = [
|
|
|
1044
1044
|
{ name: "list_bots", description: "Список ботов (id, имя, статус). Включает и свои боты, и боты, в которые тебя пригласили соредактором — у каждого есть accessRole (OWNER/ADMIN/EDITOR/VIEWER); у чужих (не OWNER) есть ownerName — имя владельца проекта. Конфликт токена (тот же Telegram-токен подключён ещё в BotHelp/Salebot/своём скрипте, 10+ минут): pausedReason=TOKEN_CONFLICT (+ pausedAt, pausedConflictHost) — другой сервис держит вебхук, бот active=false (ничего не удалено); pausedChannels[] — у работающего мультиканального бота выключены только эти каналы; conflictWarning=TOKEN_CONFLICT_POLLING (+ conflictWarningUsername) — токен опрашивает кто-то ещё, бот НЕ остановлен, но теряет часть сообщений (снимается само). Чинится вне Retensy (отключить бота там или /revoke в @BotFather + bot_change_token), затем bot_resume для паузы. Доступ = твоя текущая роль в проекте, проверяется на каждый вызов; владелец мог отозвать её без предупреждения.", inputSchema: { type: "object", properties: {} } },
|
|
1045
1045
|
{ name: "list_graphs", description: "Список сценариев САМОГО бота (без узлов). Вебхук-сценарии, которые лишь отвечают через этого бота, сюда не входят — их публикуют в вебе, в «Сценариях» автора.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
1046
1046
|
{ name: "list_channels", description: "Список каналов/групп, подключённых к боту (chatId, title, type, статус бота, дата). chatId — числовой id для условия SUBSCRIBED («Подписан на канал»).", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
1047
|
+
{ name: "catalog_item_upsert", description: "Создать или обновить товар магазина (POST /api/bots/commerce/items/upsert): по id, иначе по артикулу sku. Для нового товара обязательны name и price (в рублях). images — до 10 https-ссылок на фото (первое — главное). categoryIds — id категорий из catalog_category_list; categoryNames — названия категорий: найдутся по имени или создадутся. stock — остаток (-1 — не учитывать), active — показывать в витрине. Меняет витрину: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { id: { type: "string", description: "id товара из catalog_list (для обновления)" }, sku: { type: "string", description: "Артикул" }, name: { type: "string", description: "Название" }, description: { type: "string", description: "Описание" }, price: { type: "number", description: "Цена в рублях" }, stock: { type: "integer", description: "Остаток, -1 — не учитывать" }, active: { type: "boolean", description: "Показывать в витрине" }, images: { type: "array", items: { type: "string" }, maxItems: 10, description: "https-ссылки на фото, до 10" }, categoryIds: { type: "array", items: { type: "string" }, description: "id категорий (catalog_category_list)" }, categoryNames: { type: "array", items: { type: "string" }, description: "Названия категорий: существующие находятся по имени, отсутствующие создаются" }, confirm: { type: "boolean" } } } },
|
|
1048
|
+
{ name: "catalog_category_list", description: "Категории магазина владельца (GET /api/bots/commerce/categories): id, name, parentId. Только чтение.", inputSchema: { type: "object", properties: {} } },
|
|
1049
|
+
{ name: "catalog_category_upsert", description: "Создать или переименовать категорию магазина: с id — обновить (PUT /api/bots/commerce/categories/{id}), без id — создать (POST /api/bots/commerce/categories). parentId — id родительской категории; без него категория верхнего уровня. При обновлении без parentId текущий родитель сохраняется. Меняет витрину: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { id: { type: "string", description: "id категории из catalog_category_list (для обновления)" }, name: { type: "string", description: "Название категории" }, parentId: { type: "string", description: "id родительской категории" }, confirm: { type: "boolean" } }, required: ["name"] } },
|
|
1047
1050
|
{ name: "integration_catalog", description: "Каталог сервисов Integration Core (GET /api/integrations/catalog): [{provider, name, category, authType, configSchema: [{key, label, hint, secret}] — поля подключения, actions: [{kind, label, inputs}] — действия для сценария и coreDelivery, healthCheck — умеет ли integration_test, inbound: {events: [{key, label, vars: [{key, label, example}], paymentSucceeded}]} | null — провайдер умеет только исходящие действия}]. inbound.events — события для узла TRIGGER_WEBHOOK с заполненным config.provider (см. integration_ingress_url): events[].key идёт в config.event, vars[].key — переменные {{body.<key>}} в сценарии. paymentSucceeded:true — событие сверх обычного вебхука ещё и заводит узел «Оплата прошла» (headless или в чате подписчика, если платёжная ссылка была привязана к нему действием *_link). 404 — Integration Core выключен в сервисе. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1048
1051
|
{ name: "integration_status", description: "Статус подключения (GET /api/integrations/{id}/status): {status: UNKNOWN | OK | NEEDS_REAUTH (ключ отозван/устарел — обнови через connect_integration) | ERROR, lastCheckedAt, lastError, supported}. Только своё подключение (чужое — 403). Read-only, внешний сервис не вызывает.", inputSchema: { type: "object", properties: { connectionId: { type: "string", description: "id из list_integrations" } }, required: ["connectionId"] } },
|
|
1049
1052
|
{ name: "integration_test", description: "Проверить подключение (POST /api/integrations/{id}/test): выполняет ЖИВУЮ проверку ключа во внешнем сервисе от имени владельца (без побочных эффектов — ничего не создаёт) и обновляет статус. Ответ как у integration_status; supported:false — сервис проверку не умеет. Только своё подключение (чужое — 403); 404 — Integration Core выключен.", inputSchema: { type: "object", properties: { connectionId: { type: "string", description: "id из list_integrations" } }, required: ["connectionId"] } },
|
|
@@ -1262,7 +1265,7 @@ const TOOLS = [
|
|
|
1262
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"] } },
|
|
1263
1266
|
// ---- Вебхук-сценарии (/api/scenarios, PAT-доступ открыт решением владельца 14.10) ----
|
|
1264
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: {} } },
|
|
1265
|
-
{ 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"] } },
|
|
1266
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"] } },
|
|
1267
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"] } },
|
|
1268
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"] } },
|
|
@@ -1307,6 +1310,47 @@ async function handleCall(params) {
|
|
|
1307
1310
|
case "list_graphs": return okResult(await api(`/api/bots/${a.botId}/graphs`));
|
|
1308
1311
|
case "list_channels": return okResult(await api(`/api/bots/${a.botId}/linked-chats`));
|
|
1309
1312
|
case "list_integrations": return okResult(withoutSecrets(await api("/api/bots/integrations")));
|
|
1313
|
+
case "catalog_item_upsert": {
|
|
1314
|
+
if (a.confirm !== true) throw new Error("Сохранение товара меняет витрину. Спроси пользователя и повтори с confirm:true.");
|
|
1315
|
+
if (a.images !== undefined) {
|
|
1316
|
+
if (!Array.isArray(a.images) || a.images.length > 10) throw new Error("images: массив до 10 https-ссылок");
|
|
1317
|
+
for (const u of a.images) {
|
|
1318
|
+
let ok = false;
|
|
1319
|
+
try { ok = new URL(String(u)).protocol === "https:"; } catch { ok = false; }
|
|
1320
|
+
if (!ok) throw new Error(`images: нужна https-ссылка, получено «${u}»`);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
let categoryIds = a.categoryIds;
|
|
1324
|
+
if (Array.isArray(a.categoryNames) && a.categoryNames.length) {
|
|
1325
|
+
const cats = await api("/api/bots/commerce/categories");
|
|
1326
|
+
categoryIds = Array.isArray(categoryIds) ? [...categoryIds] : [];
|
|
1327
|
+
for (const raw of a.categoryNames) {
|
|
1328
|
+
const catName = String(raw ?? "").trim();
|
|
1329
|
+
if (!catName) continue;
|
|
1330
|
+
let cat = cats.find((c) => String(c.name).toLowerCase() === catName.toLowerCase());
|
|
1331
|
+
if (!cat) { cat = await api("/api/bots/commerce/categories", { method: "POST", body: { name: catName } }); cats.push(cat); }
|
|
1332
|
+
if (!categoryIds.includes(cat.id)) categoryIds.push(cat.id);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const body = { id: a.id || undefined, sku: a.sku, name: a.name, description: a.description, price: a.price,
|
|
1336
|
+
stock: a.stock, active: a.active, images: a.images, categoryIds };
|
|
1337
|
+
return okResult(await api("/api/bots/commerce/items/upsert", { method: "POST", body }));
|
|
1338
|
+
}
|
|
1339
|
+
case "catalog_category_list": return okResult(await api("/api/bots/commerce/categories"));
|
|
1340
|
+
case "catalog_category_upsert": {
|
|
1341
|
+
if (a.confirm !== true) throw new Error("Изменение категорий меняет витрину. Спроси пользователя и повтори с confirm:true.");
|
|
1342
|
+
const catName = String(a.name ?? "").trim();
|
|
1343
|
+
if (!catName) throw new Error("Укажите название категории.");
|
|
1344
|
+
if (a.id) {
|
|
1345
|
+
let parentId = a.parentId;
|
|
1346
|
+
if (parentId === undefined) {
|
|
1347
|
+
const cats = await api("/api/bots/commerce/categories");
|
|
1348
|
+
parentId = cats.find((c) => c.id === a.id)?.parentId ?? null;
|
|
1349
|
+
}
|
|
1350
|
+
return okResult(await api(`/api/bots/commerce/categories/${encodeURIComponent(String(a.id))}`, { method: "PUT", body: { name: catName, parentId } }));
|
|
1351
|
+
}
|
|
1352
|
+
return okResult(await api("/api/bots/commerce/categories", { method: "POST", body: { name: catName, parentId: a.parentId ?? null } }));
|
|
1353
|
+
}
|
|
1310
1354
|
case "integration_catalog": return okResult(withoutSecrets(await api("/api/integrations/catalog")));
|
|
1311
1355
|
case "integration_status":
|
|
1312
1356
|
case "integration_test": {
|