@retensy/mcp 0.15.1 → 0.16.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 +1 -1
- package/skills/send-broadcast/SKILL.md +2 -1
- package/src/index.mjs +63 -14
package/README.md
CHANGED
|
@@ -140,13 +140,13 @@ MCP-сервер (+ скилл для Claude Code) для **сборки и пу
|
|
|
140
140
|
| `site_leads(siteId, page?, size?)` | заявки из форм (поля, UTM, статус доставки) |
|
|
141
141
|
| `site_lead_settings(siteId, settings?)` | куда доставлять заявки: бот уведомлений, почта, вебхук, вебхук-сценарий, amoCRM, «Интеграция» (`coreDelivery {connectionId, kind, params}` из `coreConnections`) |
|
|
142
142
|
| `broadcast_list(botId?, group?, page?, size?)` | рассылки + счётчики разделов (черновики/запланированные/отправленные/повторы) |
|
|
143
|
-
| `broadcast_get(broadcastId)` | рассылка целиком: статус, счётчики,
|
|
143
|
+
| `broadcast_get(broadcastId)` | рассылка целиком: статус, счётчики, сообщения, причины ошибок |
|
|
144
144
|
| `broadcast_preview(botIds, tagsAll?, tagsNone?)` | сколько подписчиков получат рассылку |
|
|
145
145
|
| `broadcast_send(name, botIds, messages \| graphId, tagsAll?, tagsNone?, scheduledAt?, draftId?)` | отправить сейчас / запланировать; по нескольким ботам; или запуск сценария |
|
|
146
146
|
| `broadcast_cancel(broadcastId)` | отменить запланированную/идущую |
|
|
147
147
|
| `broadcast_recurring(action, …)` | повторяющиеся рассылки: list / create (DAILY·MONTHLY·YEARLY) / stop |
|
|
148
148
|
| `broadcast_drafts(action, …)` | черновики: list / get / create / update / delete |
|
|
149
|
-
| `broadcast_duplicate(broadcastId)` | копия рассылки
|
|
149
|
+
| `broadcast_duplicate(broadcastId` или `draftId)` | копия рассылки или черновика — новый черновик «… (копия)» |
|
|
150
150
|
| `article_list()` | свои статьи блога (id, slug, title, просмотры) |
|
|
151
151
|
| `article_get(slug)` | статья по slug (Markdown content, excerpt, обложка) |
|
|
152
152
|
| `article_publish(content, title?, cover?, excerpt?)` | новая статья (Markdown; title из `# ...`, если не задан; обложка из `cover`-URL или 1-й картинки → OG; `excerpt` явно или авто) → id, slug, URL |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retensy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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": {
|
|
@@ -16,7 +16,8 @@ description: Сделать рассылку подписчикам ботов r
|
|
|
16
16
|
5. `broadcast_send {name, botIds, messages, tagsAll?, tagsNone?, scheduledAt?}` → `{broadcastIds, totalAudience}`.
|
|
17
17
|
Не уверен в тексте — сначала `broadcast_drafts {action: "create", …}`: пользователь увидит черновик в кабинете,
|
|
18
18
|
отправка — `broadcast_send {draftId}` (черновик после отправки удаляется).
|
|
19
|
-
6. Статус — `broadcast_get {broadcastId}` / `broadcast_list`; остановить — `broadcast_cancel`.
|
|
19
|
+
6. Статус — `broadcast_get {broadcastId}` (с причинами ошибок) / `broadcast_list`; остановить — `broadcast_cancel`.
|
|
20
|
+
Повторить или взять за основу — `broadcast_duplicate {broadcastId | draftId}` → черновик «… (копия)».
|
|
20
21
|
|
|
21
22
|
## Сообщение
|
|
22
23
|
|
package/src/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import os from "node:os";
|
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
|
|
30
|
-
const VERSION = "0.
|
|
30
|
+
const VERSION = "0.16.0";
|
|
31
31
|
/** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
|
|
32
32
|
const SITE_INITS = ["starter", "blank", "mini-landing"];
|
|
33
33
|
/** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
|
|
@@ -648,11 +648,11 @@ const TOOLS = [
|
|
|
648
648
|
{ name: "bot_stop", description: "Остановить бота (POST /api/bots/{botId}/stop): снимает вебхук, бот перестаёт отвечать, сценарии и подписчики сохраняются. Запуск обратно — bot_resume.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
649
649
|
{ name: "bot_resume", description: "Запустить остановленного бота или бота, приостановленного лимитом тарифа (POST /api/bots/{botId}/resume). Если лимит ботов тарифа исчерпан — HTTP 402 со ссылкой на смену тарифа.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
650
650
|
// ---- Подключения ----
|
|
651
|
-
{ name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Креды хранятся в сервисе зашифрованными и НИКОГДА не возвращаются — ни здесь, ни в list_integrations (только маска hint); в отчёты не попадают. Проверить ключ после подключения — integration_test.", inputSchema: { type: "object", properties: { provider: { type: "string", description: "AMOCRM | BITRIX24 | GETCOURSE | YAMETRIKA | YOOKASSA | GOOGLE_SHEETS | INSTAGRAM" }, title: { type: "string", description: "Название подключения в кабинете (например «amoCRM продажи»)" }, creds: { type: "object", description: "Поля
|
|
651
|
+
{ name: "connect_integration", description: "Подключить сервис (POST /api/bots/integrations) — дальше его id (= connectionId) ставится в действия сценария и в site_lead_settings. provider и creds: AMOCRM {subdomain, longToken} · BITRIX24 {webhookUrl} · GETCOURSE {account, apiKey} · YAMETRIKA {counterId, oauthToken} · YOOKASSA {shopId, secretKey}. Остальные ~25 сервисов (smsru, retailcrm, cloudpayments, rest_api, yandex_market и т.д.) берутся из каталога Integration Core (integration_catalog) — provider передавай его provider-ключом в НИЖНЕМ регистре, нужные creds — из configSchema каждого провайдера (hint подскажет, где взять; поле с «необязательно»/optional в hint можно не слать). Без нужных creds вернёт, какие поля и где их взять. connectionId — обновить креды/название существующего подключения (PUT). Сервисы со входом через браузер не падают, а возвращают ссылку для пользователя: GOOGLE_SHEETS → ссылка согласия Google (OAuth; после неё таблицы выбираются в узле «Google Таблицы»), INSTAGRAM → кабинет (вход через Facebook, сейчас выключен). TELEGRAM/MAX — это боты: используй create_bot. Провайдер каталога со статусом COMING_SOON/IN_DEVELOPMENT подключить нельзя (вернёт ошибку). Если каталог недоступен (офлайн/выключен) — работают только 5 легаси-провайдеров выше. Креды хранятся в сервисе зашифрованными и НИКОГДА не возвращаются — ни здесь, ни в list_integrations (только маска hint); в отчёты не попадают. Проверить ключ после подключения — integration_test.", inputSchema: { type: "object", properties: { provider: { type: "string", description: "AMOCRM | BITRIX24 | GETCOURSE | YAMETRIKA | YOOKASSA | GOOGLE_SHEETS | INSTAGRAM, либо провайдер из integration_catalog (его ключ в нижнем регистре, напр. smsru, retailcrm, cloudpayments)" }, title: { type: "string", description: "Название подключения в кабинете (например «amoCRM продажи»)" }, creds: { type: "object", description: "Поля провайдера: для легаси — см. описание, для остальных — ключи из configSchema провайдера в integration_catalog" }, connectionId: { type: "string", description: "id существующего подключения (list_integrations) — обновить его" } }, required: ["provider"] } },
|
|
652
652
|
{ name: "disconnect_integration", description: "Удалить подключение сервиса по id из list_integrations (DELETE /api/bots/integrations/{id}). Действия сценария с этим connectionId перестанут работать.", inputSchema: { type: "object", properties: { connectionId: { type: "string" } }, required: ["connectionId"] } },
|
|
653
653
|
// ---- Рассылки ----
|
|
654
654
|
{ name: "broadcast_list", description: "Рассылки. Без botId — по всем ботам постранично (GET /api/bots/broadcasts): {counts: {drafts, scheduled, sent, recurring}, page: {content: [{id, botId, botUsername, name, status, direct, totalJobs, sentJobs, failedJobs, skippedByQuota, scheduledAt, createdAt}], totalElements…}}; group: scheduled (ещё не начали) | sent (идут/завершены). С botId — полная история одного бота. status: EXPANDING/MATERIALIZING/READY (ждёт) → RUNNING → DONE | CANCELLING → CANCELLED | FAILED. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, group: { type: "string", enum: ["scheduled", "sent"] }, page: { type: "number" }, size: { type: "number", description: "до 100, по умолчанию 20" } } } },
|
|
655
|
-
{ name: "broadcast_get", description: "Рассылка целиком по id (GET /api/bots/broadcasts/{id}): статус, счётчики отправки, фильтр аудитории, сообщения, время. Read-only.", inputSchema: { type: "object", properties: { broadcastId: { type: "string" } }, required: ["broadcastId"] } },
|
|
655
|
+
{ name: "broadcast_get", description: "Рассылка целиком по id (GET /api/bots/broadcasts/{id}): статус, счётчики отправки, фильтр аудитории, сообщения, время. При failedJobs > 0 — ещё errors: топ-5 причин ошибок [{error, count}] (GET …/{id}/errors; детали живут 30 дней). Read-only.", inputSchema: { type: "object", properties: { broadcastId: { type: "string" } }, required: ["broadcastId"] } },
|
|
656
656
|
{ name: "broadcast_preview", description: "Сколько подписчиков получат рассылку (POST /api/bots/{botId}/broadcasts/preview) — по каждому боту и всего. Фильтр по тегам: tagsAll — есть ВСЕ эти теги, tagsNone — нет НИ ОДНОГО; без тегов — все подписчики бота. Лимит — 50 000 получателей на бота. Ничего не отправляет.", inputSchema: { type: "object", properties: { botIds: { type: "array", items: { type: "string" } }, botId: { type: "string" }, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } } } } },
|
|
657
657
|
{ name: "broadcast_send", description: "Отправить рассылку сейчас или запланировать (scheduledAt). " +
|
|
658
658
|
"Прямая (по умолчанию, POST /api/bots/broadcasts/direct): name, botIds[] (1–20 ботов ОДНОГО владельца; по каждому создаётся своя рассылка), messages[] (1–5), tagsAll?/tagsNone? (фильтр по тегам). " +
|
|
@@ -664,7 +664,7 @@ const TOOLS = [
|
|
|
664
664
|
{ name: "broadcast_cancel", description: "Отменить рассылку (POST /api/bots/broadcasts/{id}/cancel): запланированная не уйдёт, идущая остановится (статус CANCELLING → CANCELLED). Уже завершённую отменить нельзя — HTTP 409.", inputSchema: { type: "object", properties: { broadcastId: { type: "string" } }, required: ["broadcastId"] } },
|
|
665
665
|
{ name: "broadcast_recurring", description: "Повторяющиеся рассылки (/api/bots/broadcasts/recurring). action: list — правила (активные и остановленные); create {name, botIds[], messages[], tagsAll?, tagsNone?, recurrence: DAILY|MONTHLY|YEARLY, firstRunAt} — сообщения как в broadcast_send, 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" } }, recurrence: { type: "string", enum: ["DAILY", "MONTHLY", "YEARLY"] }, firstRunAt: { type: "string" } }, required: ["action"] } },
|
|
666
666
|
{ 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"] } },
|
|
667
|
-
{ name: "broadcast_duplicate", description: "Копия
|
|
667
|
+
{ name: "broadcast_duplicate", description: "Копия как новый черновик «<имя> (копия)»: исходник не меняется, время отправки и статистика не переносятся. broadcastId — прямая рассылка (POST /api/bots/broadcasts/{id}/duplicate: бот, фильтр, сообщения; по сценарию — HTTP 409); draftId — черновик (POST /api/bots/broadcasts/drafts/{id}/duplicate). Дальше broadcast_drafts update / broadcast_send {draftId}.", inputSchema: { type: "object", properties: { broadcastId: { type: "string" }, draftId: { type: "string" } } } },
|
|
668
668
|
// ---- Сайты: библиотека шаблонов ----
|
|
669
669
|
{ 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" } } } },
|
|
670
670
|
];
|
|
@@ -1000,26 +1000,69 @@ async function handleCall(params) {
|
|
|
1000
1000
|
{ connectedGoogleAccounts: connected });
|
|
1001
1001
|
}
|
|
1002
1002
|
const spec = PROVIDER_FIELDS[provider];
|
|
1003
|
-
if (
|
|
1003
|
+
if (spec) {
|
|
1004
|
+
// Легаси-провайдер (одна из исходных 5) — шлём enum `provider` как раньше, без каталога.
|
|
1005
|
+
const creds = a.creds && typeof a.creds === "object" ? Object.fromEntries(
|
|
1006
|
+
Object.entries(a.creds).filter(([, v]) => v != null && String(v).trim() !== "").map(([k, v]) => [k, String(v).trim()])) : {};
|
|
1007
|
+
const missing = Object.keys(spec.fields).filter((k) => !creds[k]);
|
|
1008
|
+
if (missing.length && (!a.connectionId || Object.keys(creds).length)) {
|
|
1009
|
+
return okResult({
|
|
1010
|
+
connected: false,
|
|
1011
|
+
provider,
|
|
1012
|
+
need: Object.fromEntries(missing.map((k) => [k, spec.fields[k]])),
|
|
1013
|
+
instruction: `Для ${spec.name} не хватает полей creds: ${missing.join(", ")}. Попроси их у пользователя и вызови connect_integration ещё раз. Или пусть подключит сам в кабинете: ${CONNECT_PAGE}`,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
const title = a.title || spec.name;
|
|
1017
|
+
try {
|
|
1018
|
+
const saved = a.connectionId
|
|
1019
|
+
? await api(`/api/bots/integrations/${a.connectionId}`, { method: "PUT", body: { title: a.title, creds: Object.keys(creds).length ? creds : undefined } })
|
|
1020
|
+
: await api("/api/bots/integrations", { method: "POST", body: { provider, title, creds } });
|
|
1021
|
+
return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (amocrm_send, bitrix24_call, getcourse_send, yametrika_event, оплата ЮKassa) и в site_lead_settings (amoConnectionId или coreDelivery.connectionId). Проверить ключ — integration_test." });
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
if (e.status === 400) throw new Error(`${spec.name} не подключён: ${bodyReason(e.data) || "креды не приняты"}. Проверь поля: ${Object.entries(spec.fields).map(([k, v]) => `${k} — ${v}`).join("; ")}.`);
|
|
1024
|
+
throw e;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Провайдер из каталога Integration Core (~25 сервисов, GET /api/integrations/catalog) —
|
|
1029
|
+
// ключ в запросе на создание ВСЁ ЕЩЁ называется `provider`, но в нижнем регистре (bots/integrations
|
|
1030
|
+
// одинаково принимает enum-провайдеров и свободные providerKey из реестра, см. IntegrationConnectionService.create).
|
|
1031
|
+
const catalogKey = String(a.provider || "").trim().toLowerCase().replace(/[\s.-]+/g, "_");
|
|
1032
|
+
let catalog;
|
|
1033
|
+
try { catalog = await api("/api/integrations/catalog"); } catch { /* офлайн — фолбэк на статическую таблицу ниже */ }
|
|
1034
|
+
if (!Array.isArray(catalog)) {
|
|
1035
|
+
throw new Error(`Неизвестный provider «${a.provider}». Бывают: ${Object.keys(PROVIDER_FIELDS).join(", ")}, GOOGLE_SHEETS, INSTAGRAM (каталог Integration Core сейчас недоступен — офлайн-режим). Каталог: ${CONNECT_PAGE}`);
|
|
1036
|
+
}
|
|
1037
|
+
const entry = catalog.find((p) => String(p?.provider || "").toLowerCase() === catalogKey);
|
|
1038
|
+
if (!entry) {
|
|
1039
|
+
const names = catalog.map((p) => p.provider).filter(Boolean).join(", ");
|
|
1040
|
+
throw new Error(`Неизвестный provider «${a.provider}». Бывают: ${Object.keys(PROVIDER_FIELDS).join(", ")}, GOOGLE_SHEETS, INSTAGRAM, ${names}. Каталог: ${CONNECT_PAGE}`);
|
|
1041
|
+
}
|
|
1042
|
+
if (entry.status === "COMING_SOON" || entry.status === "IN_DEVELOPMENT") {
|
|
1043
|
+
throw new Error(`${entry.name} (${entry.provider}) пока нельзя подключить: статус ${entry.status}. Каталог: ${CONNECT_PAGE}`);
|
|
1044
|
+
}
|
|
1045
|
+
const schema = Array.isArray(entry.configSchema) ? entry.configSchema : [];
|
|
1004
1046
|
const creds = a.creds && typeof a.creds === "object" ? Object.fromEntries(
|
|
1005
1047
|
Object.entries(a.creds).filter(([, v]) => v != null && String(v).trim() !== "").map(([k, v]) => [k, String(v).trim()])) : {};
|
|
1006
|
-
const
|
|
1048
|
+
const requiredFields = schema.filter((f) => !/необязательно|optional/i.test(f?.hint || ""));
|
|
1049
|
+
const missing = requiredFields.map((f) => f.key).filter((k) => !creds[k]);
|
|
1007
1050
|
if (missing.length && (!a.connectionId || Object.keys(creds).length)) {
|
|
1008
1051
|
return okResult({
|
|
1009
1052
|
connected: false,
|
|
1010
|
-
provider,
|
|
1011
|
-
need: Object.fromEntries(missing.map((
|
|
1012
|
-
instruction: `Для ${
|
|
1053
|
+
provider: entry.provider,
|
|
1054
|
+
need: Object.fromEntries(schema.filter((f) => missing.includes(f.key)).map((f) => [f.key, f.hint || f.label])),
|
|
1055
|
+
instruction: `Для ${entry.name} не хватает полей creds: ${missing.join(", ")}. Попроси их у пользователя и вызови connect_integration ещё раз. Или пусть подключит сам в кабинете: ${CONNECT_PAGE}`,
|
|
1013
1056
|
});
|
|
1014
1057
|
}
|
|
1015
|
-
const title = a.title ||
|
|
1058
|
+
const title = a.title || entry.name;
|
|
1016
1059
|
try {
|
|
1017
1060
|
const saved = a.connectionId
|
|
1018
1061
|
? await api(`/api/bots/integrations/${a.connectionId}`, { method: "PUT", body: { title: a.title, creds: Object.keys(creds).length ? creds : undefined } })
|
|
1019
|
-
: await api("/api/bots/integrations", { method: "POST", body: { provider, title, creds } });
|
|
1020
|
-
return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (
|
|
1062
|
+
: await api("/api/bots/integrations", { method: "POST", body: { provider: entry.provider, title, creds } });
|
|
1063
|
+
return okResult({ connected: true, connectionId: saved?.id, ...withoutSecrets(saved), note: "Креды сохранены зашифрованными и обратно не отдаются (только маска hint).", usage: "connectionId ставь в действия сценария (actions из integration_catalog) и в site_lead_settings (coreDelivery.connectionId). Проверить ключ — integration_test." });
|
|
1021
1064
|
} catch (e) {
|
|
1022
|
-
if (e.status === 400) throw new Error(`${
|
|
1065
|
+
if (e.status === 400) throw new Error(`${entry.name} не подключён: ${bodyReason(e.data) || "креды не приняты"}. Проверь поля: ${schema.map((f) => `${f.key} — ${f.hint || f.label}`).join("; ")}.`);
|
|
1023
1066
|
throw e;
|
|
1024
1067
|
}
|
|
1025
1068
|
}
|
|
@@ -1034,7 +1077,11 @@ async function handleCall(params) {
|
|
|
1034
1077
|
try { counts = await api("/api/bots/broadcasts/counts"); } catch { /* счётчики не обязательны */ }
|
|
1035
1078
|
return okResult({ counts, page });
|
|
1036
1079
|
}
|
|
1037
|
-
case "broadcast_get":
|
|
1080
|
+
case "broadcast_get": {
|
|
1081
|
+
const b = await api(`/api/bots/broadcasts/${a.broadcastId}`);
|
|
1082
|
+
if (b && b.failedJobs > 0) b.errors = await api(`/api/bots/broadcasts/${a.broadcastId}/errors`).catch(() => []);
|
|
1083
|
+
return okResult(b);
|
|
1084
|
+
}
|
|
1038
1085
|
case "broadcast_preview": {
|
|
1039
1086
|
const ids = botIdsOf(a);
|
|
1040
1087
|
await assertBroadcastBots(ids);
|
|
@@ -1131,6 +1178,8 @@ async function handleCall(params) {
|
|
|
1131
1178
|
throw new Error("action: list | get | create | update | delete.");
|
|
1132
1179
|
}
|
|
1133
1180
|
case "broadcast_duplicate":
|
|
1181
|
+
if (a.draftId) return okResult(await api(`/api/bots/broadcasts/drafts/${a.draftId}/duplicate`, { method: "POST" }));
|
|
1182
|
+
if (!a.broadcastId) throw new Error("Передай broadcastId или draftId.");
|
|
1134
1183
|
return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/duplicate`, { method: "POST" }));
|
|
1135
1184
|
case "site_templates": {
|
|
1136
1185
|
const r = await api("/api/bots/pages/templates");
|