@retensy/mcp 0.21.2 → 0.24.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +106 -7
- package/package.json +2 -2
- package/skills/build-bot-funnel/SKILL.md +1 -0
- package/skills/build-bot-funnel/reference/schema.md +14 -3
- package/skills/build-bot-funnel/reference/validation.md +8 -0
- package/skills/build-bot-funnel/validate.mjs +2 -2
- package/skills/rebuild-funnel-from-builder/SKILL.md +154 -0
- package/skills/rebuild-funnel-from-builder/reference/mapping.md +95 -0
- package/src/index.mjs +592 -25
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.24.0";
|
|
31
31
|
/** С чего начать пустой сайт (init у /document/ops; на сайте с черновиком игнорируется). */
|
|
32
32
|
const SITE_INITS = ["starter", "blank", "mini-landing"];
|
|
33
33
|
/** Безвредная операция, когда нужен только init: бэкенд не принимает пустой ops[]. */
|
|
@@ -414,6 +414,78 @@ async function uploadSiteAsset(siteId, { filePath, url }) {
|
|
|
414
414
|
return { asset: `assets/${safe}`, sizeBytes: bytes.length };
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
// POST с бинарным (не JSON) ответом — audience_export format=xlsx (zip/binary; res.text() испортил бы байты UTF-8-декодом).
|
|
418
|
+
// Ошибки — тот же разбор, что у api(), код не шарим напрямую (api() не отдаёт сырые байты при res.ok).
|
|
419
|
+
async function apiBinaryPost(path_, body) {
|
|
420
|
+
if (!isAuthed()) throw new Error(NO_AUTH_HELP);
|
|
421
|
+
const res = await fetch(`${BASE}${path_}`, { method: "POST", headers: authHeaders(), body: JSON.stringify(body) });
|
|
422
|
+
if (!res.ok) {
|
|
423
|
+
const text = await res.text().catch(() => "");
|
|
424
|
+
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
425
|
+
if (res.status === 401) throw new Error(`Доступ отклонён (HTTP 401). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
426
|
+
if (res.status === 403) throw new Error(`Доступ отклонён (HTTP 403)${bodyReason(data) ? `: ${bodyReason(data)}` : ""}.`);
|
|
427
|
+
if (res.status === 402) throw paymentError(data);
|
|
428
|
+
throw httpError("POST", path_, res.status, data);
|
|
429
|
+
}
|
|
430
|
+
return Buffer.from(await res.arrayBuffer());
|
|
431
|
+
}
|
|
432
|
+
// HEAD, не GET — заявки могут нести бинарные вложения; нам нужны только заголовки (имя/тип/размер),
|
|
433
|
+
// без скачивания байтов файла в MCP-ответ (см. site_lead_file_download). Spring отвечает на @GetMapping
|
|
434
|
+
// заголовками без тела и для HEAD — метод контроллера при этом всё равно выполняется целиком (читает файл
|
|
435
|
+
// с диска/объектного хранилища), просто DispatcherServlet не отдаёт тело — доступ/404 проверяются так же, как у GET.
|
|
436
|
+
async function siteLeadFileHead(siteId, leadId, fileId) {
|
|
437
|
+
if (!isAuthed()) throw new Error(NO_AUTH_HELP);
|
|
438
|
+
const path_ = `/api/bots/pages/${siteId}/leads/${leadId}/files/${fileId}`;
|
|
439
|
+
const res = await fetch(`${BASE}${path_}`, { method: "HEAD", headers: authHeaders() });
|
|
440
|
+
if (!res.ok) {
|
|
441
|
+
if (res.status === 401) throw new Error(`Доступ отклонён (HTTP 401). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
442
|
+
if (res.status === 403) throw new Error(`Доступ отклонён (HTTP 403). Нет прав на заявки этого сайта.`);
|
|
443
|
+
if (res.status === 404) throw new Error("Файл не найден: не принадлежит этой заявке/сайту, либо заявка/файл удалены.");
|
|
444
|
+
throw httpError("HEAD", path_, res.status, null);
|
|
445
|
+
}
|
|
446
|
+
const disp = res.headers.get("content-disposition") || "";
|
|
447
|
+
const m = disp.match(/filename="([^"]*)"/);
|
|
448
|
+
const len = res.headers.get("content-length");
|
|
449
|
+
return {
|
|
450
|
+
fileId, siteId, leadId,
|
|
451
|
+
fileName: m ? m[1] : null,
|
|
452
|
+
contentType: res.headers.get("content-type") || null,
|
|
453
|
+
sizeBytes: len ? Number(len) : null,
|
|
454
|
+
downloadUrl: `${BASE}${path_}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function saveBinary(buf, savePath) {
|
|
458
|
+
const abs = path.resolve(String(savePath).replace(/^~(?=$|[/\\])/, os.homedir()));
|
|
459
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
460
|
+
fs.writeFileSync(abs, buf);
|
|
461
|
+
return okResult({ saved: abs, bytes: buf.length });
|
|
462
|
+
}
|
|
463
|
+
// Миграция подписчиков (multipart: file + platform + mapping?) — свой fetch, как у uploadMedia/uploadSiteAsset:
|
|
464
|
+
// api() ставит Content-Type:application/json, multipart сам задаёт boundary через FormData.
|
|
465
|
+
async function migrationMultipart(path_, { filePath, platform, mapping }) {
|
|
466
|
+
if (!isAuthed()) throw new Error(NO_AUTH_HELP);
|
|
467
|
+
if (!filePath) throw new Error("Передай path — локальный CSV-файл.");
|
|
468
|
+
const abs = path.resolve(String(filePath).replace(/^~(?=$|[/\\])/, os.homedir()));
|
|
469
|
+
let bytes;
|
|
470
|
+
try { bytes = fs.readFileSync(abs); } catch { throw new Error(`Файл не найден: ${abs}`); }
|
|
471
|
+
const headers = {};
|
|
472
|
+
const token = getToken(); const cookie = getCookie();
|
|
473
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
474
|
+
else if (cookie) headers.Cookie = cookie;
|
|
475
|
+
const fd = new FormData();
|
|
476
|
+
fd.append("file", new Blob([bytes], { type: "text/csv" }), path.basename(abs));
|
|
477
|
+
fd.append("platform", platform);
|
|
478
|
+
if (mapping != null) fd.append("mapping", typeof mapping === "string" ? mapping : JSON.stringify(mapping));
|
|
479
|
+
const res = await fetch(`${BASE}${path_}`, { method: "POST", headers, body: fd });
|
|
480
|
+
const text = await res.text();
|
|
481
|
+
let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
482
|
+
if (!res.ok) {
|
|
483
|
+
if (res.status === 401 || res.status === 403) throw new Error(`Доступ отклонён (HTTP ${res.status}). Токен невалиден/отозван — создай новый на ${TOKENS_PAGE}.`);
|
|
484
|
+
throw httpError("POST", path_, res.status, data);
|
|
485
|
+
}
|
|
486
|
+
return data;
|
|
487
|
+
}
|
|
488
|
+
|
|
417
489
|
const okResult = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] });
|
|
418
490
|
const errResult = (e) => ({ isError: true, content: [{ type: "text", text: "❌ " + (e?.message || String(e)) }] });
|
|
419
491
|
|
|
@@ -486,6 +558,15 @@ function dateArg(v, label) {
|
|
|
486
558
|
return s;
|
|
487
559
|
}
|
|
488
560
|
const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$|^24:00$/;
|
|
561
|
+
// hours и breaks (BookingCalendar) — одна и та же форма {day: 1–7, from: "HH:mm", to: "HH:mm"}.
|
|
562
|
+
function weekHoursArg(list, label) {
|
|
563
|
+
if (!Array.isArray(list)) throw new Error(`${label} — массив [{day: 1–7, from: "HH:mm", to: "HH:mm"}].`);
|
|
564
|
+
list.forEach((h, i) => {
|
|
565
|
+
if (!Number.isInteger(h?.day) || h.day < 1 || h.day > 7) throw new Error(`${label}[${i}].day — от 1 (пн) до 7 (вс).`);
|
|
566
|
+
if (!HHMM_RE.test(String(h.from)) || !HHMM_RE.test(String(h.to))) throw new Error(`${label}[${i}]: from/to — время HH:mm.`);
|
|
567
|
+
});
|
|
568
|
+
return list.map((h) => ({ day: h.day, from: h.from, to: h.to }));
|
|
569
|
+
}
|
|
489
570
|
// Поля календаря записи (BookingCalendar): переданные накладываются на base (для PUT — текущий календарь целиком).
|
|
490
571
|
function calendarBody(a, base = {}) {
|
|
491
572
|
const b = { ...base };
|
|
@@ -497,14 +578,7 @@ function calendarBody(a, base = {}) {
|
|
|
497
578
|
if (!Number.isInteger(n) || n < 5 || n > 1440) throw new Error("slotMinutes — целое от 5 до 1440.");
|
|
498
579
|
b.slotMinutes = n;
|
|
499
580
|
}
|
|
500
|
-
if (a.hours != null)
|
|
501
|
-
if (!Array.isArray(a.hours)) throw new Error("hours — массив [{day: 1–7, from: \"HH:mm\", to: \"HH:mm\"}].");
|
|
502
|
-
a.hours.forEach((h, i) => {
|
|
503
|
-
if (!Number.isInteger(h?.day) || h.day < 1 || h.day > 7) throw new Error(`hours[${i}].day — от 1 (пн) до 7 (вс).`);
|
|
504
|
-
if (!HHMM_RE.test(String(h.from)) || !HHMM_RE.test(String(h.to))) throw new Error(`hours[${i}]: from/to — время HH:mm.`);
|
|
505
|
-
});
|
|
506
|
-
b.hours = a.hours.map((h) => ({ day: h.day, from: h.from, to: h.to }));
|
|
507
|
-
}
|
|
581
|
+
if (a.hours != null) b.hours = weekHoursArg(a.hours, "hours");
|
|
508
582
|
if (a.exceptions != null) {
|
|
509
583
|
if (!Array.isArray(a.exceptions)) throw new Error("exceptions — массив [{date: \"ГГГГ-ММ-ДД\", from?, to?}].");
|
|
510
584
|
b.exceptions = a.exceptions.map((e, i) => {
|
|
@@ -514,6 +588,60 @@ function calendarBody(a, base = {}) {
|
|
|
514
588
|
return closed ? { date } : { date, from: e.from, to: e.to };
|
|
515
589
|
});
|
|
516
590
|
}
|
|
591
|
+
// ---- Поля из wave/audit-1014 (booking follow-up): буферы/горизонт/типы встреч/форма клиента/подтверждение/публичная ссылка ----
|
|
592
|
+
if (a.description !== undefined) {
|
|
593
|
+
const d = a.description === null ? "" : String(a.description).trim();
|
|
594
|
+
if (d.length > 500) throw new Error("description — до 500 символов.");
|
|
595
|
+
b.description = d;
|
|
596
|
+
}
|
|
597
|
+
if (a.breaks != null) b.breaks = weekHoursArg(a.breaks, "breaks");
|
|
598
|
+
if (a.meetingTypes != null) {
|
|
599
|
+
if (!Array.isArray(a.meetingTypes)) throw new Error("meetingTypes — массив [{id?, name, durationMinutes: 5–1440}].");
|
|
600
|
+
b.meetingTypes = a.meetingTypes.map((m, i) => {
|
|
601
|
+
const name = String(m?.name ?? "").trim();
|
|
602
|
+
if (!name) throw new Error(`meetingTypes[${i}].name обязателен.`);
|
|
603
|
+
const dur = Number(m?.durationMinutes);
|
|
604
|
+
if (!Number.isInteger(dur) || dur < 5 || dur > 1440) throw new Error(`meetingTypes[${i}].durationMinutes — целое от 5 до 1440.`);
|
|
605
|
+
const id = m?.id ? uuidArg(m.id, `meetingTypes[${i}].id`) : crypto.randomUUID();
|
|
606
|
+
return { id, name, durationMinutes: dur };
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const intRange = (v, label, lo, hi) => {
|
|
610
|
+
const n = Number(v);
|
|
611
|
+
if (!Number.isInteger(n) || n < lo || n > hi) throw new Error(`${label} — целое от ${lo} до ${hi}.`);
|
|
612
|
+
return n;
|
|
613
|
+
};
|
|
614
|
+
if (a.bufferBeforeMinutes != null) b.bufferBeforeMinutes = intRange(a.bufferBeforeMinutes, "bufferBeforeMinutes", 0, 240);
|
|
615
|
+
if (a.bufferAfterMinutes != null) b.bufferAfterMinutes = intRange(a.bufferAfterMinutes, "bufferAfterMinutes", 0, 240);
|
|
616
|
+
if (a.minNoticeMinutes != null) b.minNoticeMinutes = intRange(a.minNoticeMinutes, "minNoticeMinutes", 0, 10080);
|
|
617
|
+
if (a.horizonDays != null) b.horizonDays = intRange(a.horizonDays, "horizonDays", 1, 366);
|
|
618
|
+
if (a.intakeFields != null) {
|
|
619
|
+
if (!Array.isArray(a.intakeFields)) throw new Error("intakeFields — массив [{key, label, type: text|phone|email|textarea, required?}].");
|
|
620
|
+
const IF_TYPES = ["text", "phone", "email", "textarea"];
|
|
621
|
+
b.intakeFields = a.intakeFields.map((f, i) => {
|
|
622
|
+
const key = String(f?.key ?? "").trim();
|
|
623
|
+
if (!/^[A-Za-z0-9_]{1,64}$/.test(key)) throw new Error(`intakeFields[${i}].key — латиница/цифры/_, получено: ${key || "пусто"}.`);
|
|
624
|
+
const type = String(f?.type ?? "text").trim().toLowerCase();
|
|
625
|
+
if (!IF_TYPES.includes(type)) throw new Error(`intakeFields[${i}].type: ${IF_TYPES.join(" | ")}.`);
|
|
626
|
+
return { key, label: String(f?.label ?? key).trim(), type, required: f?.required === true };
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
if (a.confirmationMode != null) b.confirmationMode = enumArg(a.confirmationMode, "confirmationMode", ["AUTO", "MANUAL"]);
|
|
630
|
+
if (a.pendingHoldHours != null) b.pendingHoldHours = intRange(a.pendingHoldHours, "pendingHoldHours", 1, 720);
|
|
631
|
+
if (a.notifyOwner != null) {
|
|
632
|
+
if (typeof a.notifyOwner !== "boolean") throw new Error("notifyOwner — true или false.");
|
|
633
|
+
b.notifyOwner = a.notifyOwner;
|
|
634
|
+
}
|
|
635
|
+
if (a.slug !== undefined) {
|
|
636
|
+
const s = a.slug === null ? null : String(a.slug).trim().toLowerCase();
|
|
637
|
+
if (s && !/^[a-z0-9]([a-z0-9-]{1,58}[a-z0-9])?$/.test(s)) throw new Error("slug — 3–60 символов: латиница/цифры/дефис, не начинается и не кончается дефисом.");
|
|
638
|
+
b.slug = s || null;
|
|
639
|
+
}
|
|
640
|
+
if (a.enabled != null) {
|
|
641
|
+
if (typeof a.enabled !== "boolean") throw new Error("enabled — true или false.");
|
|
642
|
+
if (a.enabled && !(b.slug || base.slug)) throw new Error("enabled:true требует slug (задай его тем же вызовом или раньше).");
|
|
643
|
+
b.enabled = a.enabled;
|
|
644
|
+
}
|
|
517
645
|
return b;
|
|
518
646
|
}
|
|
519
647
|
// Компактная сводка графа (без объёмных text/cards/buttons) — чтобы не упираться в лимит токенов
|
|
@@ -593,6 +721,33 @@ function toInstant(v, field) {
|
|
|
593
721
|
return d.toISOString();
|
|
594
722
|
}
|
|
595
723
|
|
|
724
|
+
// feature broadcast-dispatch: дедуп + распределённый запуск по ботам (BroadcastGroup). Оба необязательны;
|
|
725
|
+
// бэкенд сам провалидирует форму целиком — здесь только базовые проверки до запроса.
|
|
726
|
+
function staggerArg(v) {
|
|
727
|
+
if (v == null) return undefined;
|
|
728
|
+
if (typeof v !== "object" || Array.isArray(v)) throw new Error("stagger — объект {startAt?, timezone?, intervalMinutes?, botOrder?, maxPerInterval?}.");
|
|
729
|
+
const out = {};
|
|
730
|
+
if (v.startAt != null) out.startAt = toInstant(v.startAt, "stagger.startAt");
|
|
731
|
+
if (v.timezone != null) out.timezone = String(v.timezone);
|
|
732
|
+
const interval = v.intervalMinutes == null ? 0 : Number(v.intervalMinutes);
|
|
733
|
+
if (!Number.isInteger(interval) || interval < 0) throw new Error("stagger.intervalMinutes — целое ≥ 0.");
|
|
734
|
+
out.intervalMinutes = interval;
|
|
735
|
+
if (v.botOrder != null) out.botOrder = enumArg(v.botOrder, "stagger.botOrder", ["AS_LISTED", "SIZE_ASC", "SIZE_DESC"]);
|
|
736
|
+
if (v.maxPerInterval != null) {
|
|
737
|
+
const n = Number(v.maxPerInterval);
|
|
738
|
+
if (!Number.isInteger(n) || n < 1) throw new Error("stagger.maxPerInterval — целое положительное.");
|
|
739
|
+
out.maxPerInterval = n;
|
|
740
|
+
}
|
|
741
|
+
return out;
|
|
742
|
+
}
|
|
743
|
+
// AudienceFilter (feature audience) передаётся как есть — полную форму и валидацию держит бэкенд;
|
|
744
|
+
// если задан, tagsAll/tagsNone того же вызова игнорируются (как в контракте AudienceFilter).
|
|
745
|
+
function audienceFilterArg(v) {
|
|
746
|
+
if (v == null) return undefined;
|
|
747
|
+
if (typeof v !== "object" || Array.isArray(v)) throw new Error("audienceFilter — объект (tagsAll/tagsAny/tagsNone, subscribedFrom/To, lastActivityFrom/To, blocked, variableConditions, conditionsCombinator).");
|
|
748
|
+
return v;
|
|
749
|
+
}
|
|
750
|
+
|
|
596
751
|
/** У Instagram-ботов рассылок нет (окно 24 ч Meta) — отказываем до запроса. */
|
|
597
752
|
async function assertBroadcastBots(botIds) {
|
|
598
753
|
if (!botIds.length) throw new Error("Передай botIds — id ботов (list_bots), по которым рассылать.");
|
|
@@ -665,6 +820,51 @@ function instagramAnswer() {
|
|
|
665
820
|
"Instagram-аккаунт нельзя подключить по API или токену: только через кнопку «Подключить Instagram» в кабинете (/bots/instagram) — она откроет вход через Instagram. Нужен Instagram-аккаунт типа Business или Creator (обычный личный профиль не подойдёт). Открой ссылку и пройди вход — после этого аккаунт появится в list_bots.");
|
|
666
821
|
}
|
|
667
822
|
|
|
823
|
+
// Доп. поля BookingCalendar (wave/audit-1014: буферы, горизонт, типы встреч, форма клиента,
|
|
824
|
+
// подтверждение, публичная ссылка) — общие между booking_calendar_create и booking_calendar_update.
|
|
825
|
+
const BOOKING_CAL_EXTRA_PROPS = {
|
|
826
|
+
description: { type: "string", description: "До 500 символов." },
|
|
827
|
+
breaks: { type: "array", description: "Повторяющиеся перерывы (обед и т.п.), та же форма, что hours — вычитаются из рабочих окон этого дня недели", items: { type: "object", properties: { day: { type: "number" }, from: { type: "string" }, to: { type: "string" } }, required: ["day", "from", "to"] } },
|
|
828
|
+
meetingTypes: { type: "array", description: "Типы встреч своей длительности; пусто — «простой» режим (slotMinutes)", items: { type: "object", properties: { id: { type: "string", description: "id существующего типа — изменить его; без id — создать новый" }, name: { type: "string" }, durationMinutes: { type: "number", description: "5–1440" } }, required: ["name", "durationMinutes"] } },
|
|
829
|
+
bufferBeforeMinutes: { type: "number", description: "Буфер до брони, 0–240 мин (по умолчанию 0)" },
|
|
830
|
+
bufferAfterMinutes: { type: "number", description: "Буфер после брони, 0–240 мин (по умолчанию 0)" },
|
|
831
|
+
minNoticeMinutes: { type: "number", description: "Не раньше чем через N минут от «сейчас», 0–10080 (по умолчанию 0)" },
|
|
832
|
+
horizonDays: { type: "number", description: "Горизонт записи в днях от сегодня, 1–366 (по умолчанию 62)" },
|
|
833
|
+
intakeFields: { type: "array", description: "Поля формы клиента (описательно — не ограничивает, что принимает booking_create/публичная форма)", items: { type: "object", properties: { key: { type: "string", description: "латиница/цифры/_" }, label: { type: "string" }, type: { type: "string", enum: ["text", "phone", "email", "textarea"] }, required: { type: "boolean" } }, required: ["key", "label", "type"] } },
|
|
834
|
+
confirmationMode: { type: "string", enum: ["AUTO", "MANUAL"], description: "AUTO — бронь сразу активна (по умолчанию); MANUAL — ждёт booking_confirm/booking_reject" },
|
|
835
|
+
pendingHoldHours: { type: "number", description: "MANUAL: сколько часов неподтверждённая бронь держит слот, 1–720 (по умолчанию 24)" },
|
|
836
|
+
notifyOwner: { type: "boolean", description: "Уведомлять владельца о новой брони (по умолчанию true)" },
|
|
837
|
+
slug: { type: ["string", "null"], description: "Публичная ссылка /book/<slug>: 3–60 символов, латиница/цифры/дефис, глобально уникален; null — убрать" },
|
|
838
|
+
enabled: { type: "boolean", description: "Включить публичную страницу записи (требует slug)" },
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// AudienceFilter (feature audience) и StaggerConfig (feature broadcast-dispatch) — общие JSON Schema куски
|
|
842
|
+
// для нескольких инструментов (audience_search/audience_export/broadcast_send/broadcast_recurring/broadcast_group).
|
|
843
|
+
const AUDIENCE_FILTER_SCHEMA = { type: "object", description: "Полный фильтр аудитории: теги any/all/none, диапазоны дат, статус, условия по переменным (вместо/вместе с tagsAll/tagsNone верхнего уровня — если задан, они игнорируются).", properties: {
|
|
844
|
+
tagsAll: { type: "array", items: { type: "string" }, description: "Есть ВСЕ эти теги" },
|
|
845
|
+
tagsAny: { type: "array", items: { type: "string" }, description: "Есть ХОТЯ БЫ ОДИН из этих тегов" },
|
|
846
|
+
tagsNone: { type: "array", items: { type: "string" }, description: "Нет НИ ОДНОГО из этих тегов" },
|
|
847
|
+
subscribedFrom: { type: "string", description: "ISO — дата подписки от (старые сессии без даты не попадут под фильтр)" },
|
|
848
|
+
subscribedTo: { type: "string" },
|
|
849
|
+
lastActivityFrom: { type: "string", description: "ISO — последняя активность от" },
|
|
850
|
+
lastActivityTo: { type: "string" },
|
|
851
|
+
blocked: { type: ["boolean", "null"], description: "true — заблокировал бота, false — активен, null/не задано — любой" },
|
|
852
|
+
variableConditions: { type: "array", items: { type: "object", properties: {
|
|
853
|
+
key: { type: "string" }, type: { type: "string", enum: ["TEXT", "NUMBER", "DATE", "BOOL"] },
|
|
854
|
+
op: { type: "string", enum: ["EQ", "NEQ", "CONTAINS", "GT", "LT", "BETWEEN", "BEFORE", "AFTER", "IS", "EMPTY", "NOT_EMPTY"] },
|
|
855
|
+
valueText: { type: "string" }, valueNumber: { type: "number" }, valueNumber2: { type: "number" },
|
|
856
|
+
valueDate: { type: "string" }, valueDate2: { type: "string" }, valueBool: { type: "boolean" },
|
|
857
|
+
}, required: ["key", "op"] } },
|
|
858
|
+
conditionsCombinator: { type: "string", enum: ["AND", "OR"], description: "Как сочетать variableConditions между собой (остальное — всегда AND)" },
|
|
859
|
+
} };
|
|
860
|
+
const STAGGER_SCHEMA = { type: "object", description: "«Распределить отправку по ботам»: когда и с каким интервалом запускать каждого следующего. Не задано — все боты стартуют вместе (на первом тике планировщика, если включён dedup).", properties: {
|
|
861
|
+
startAt: { type: "string", description: "Момент запуска первого бота по dispatch-порядку, ISO 8601 (без пояса — московское)" },
|
|
862
|
+
timezone: { type: "string", description: "IANA-зона — только для отображения расписания" },
|
|
863
|
+
intervalMinutes: { type: "number", description: "Пауза между стартами соседних ботов/порций, минуты, ≥ 0 (0 — все сразу)" },
|
|
864
|
+
botOrder: { type: "string", enum: ["AS_LISTED", "SIZE_ASC", "SIZE_DESC"], description: "Порядок запуска ботов (по умолчанию AS_LISTED — как в botIds)" },
|
|
865
|
+
maxPerInterval: { type: "number", description: "Потолок получателей на один запуск бота — сверх него уходит следующей порцией (waveIndex+1) тем же интервалом позже; никто не теряется" },
|
|
866
|
+
} };
|
|
867
|
+
|
|
668
868
|
const TOOLS = [
|
|
669
869
|
{ name: "setup", description: "Показать статус авторизации и пошаговую инструкцию подключения. Вызывай первым, если пользователь не знает, что делать, или при ошибке доступа. Токен (zmcp_...) определяет АККАУНТ, а не один проект: доступ к каждому боту/сайту — это твоя текущая роль в нём (владелец или приглашённый соредактор), и она проверяется заново при каждом вызове.", inputSchema: { type: "object", properties: {} } },
|
|
670
870
|
{ name: "set_token", description: "Сохранить персональный токен (zmcp_...), который пользователь создал на /bots/mcp-tokens. Применяется сразу, без рестарта. Токен определяет аккаунт целиком, не один проект — список доступных проектов (свои + те, куда пригласили) виден в list_bots/site_list. Если владелец проекта уберёт тебя из соредакторов, доступ к этому проекту исчезнет сразу же, без повторной настройки токена.", inputSchema: { type: "object", properties: { token: { type: "string", description: "Секрет токена, начинается с zmcp_" } }, required: ["token"] } },
|
|
@@ -689,7 +889,7 @@ const TOOLS = [
|
|
|
689
889
|
{ name: "rename_graph", description: "Переименовать сценарий (работает и для опубликованных — имя не влияет на исполнение).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, name: { type: "string" } }, required: ["graphId", "name"] } },
|
|
690
890
|
{ name: "clone_graph", description: "Склонировать граф в новый DRAFT «… (copy)» — безопасно итерировать поверх опубликованного. Клон сценария-вебхука получает собственный путь и секрет вебхука.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
691
891
|
{ name: "copy_graph", description: "Скопировать граф в ДРУГОГО бота (в т.ч. на другую платформу). Возвращает {graphId, sourcePlatform, targetPlatform, notes[]}. notes[] помечают, что адаптировано (severity=TRANSFORM, напр. вопрос-контакт → ввод телефона текстом), что требует ручной правки (MANUAL, напр. условие SUBSCRIBED в MAX) и особенности платформы (INFO). Авто-адаптация узлов реализована для Telegram⇄MAX; при копировании в/из Instagram-бота граф копируется без трансформаций — несовместимые узлы будут отмечены при публикации (IG-allowlist). preview=true — только проверка совместимости, без копирования. Тот же бот запрещён (для дублирования есть clone_graph). Копия сценария-вебхука становится обычным сценарием бота-получателя (вход по вебхуку не переносится).", inputSchema: { type: "object", properties: { graphId: { type: "string" }, targetBotId: { type: "string", description: "id бота-получателя (см. list_bots)" }, preview: { type: "boolean", description: "true = только отчёт о совместимости, ничего не сохраняется" } }, required: ["graphId", "targetBotId"] } },
|
|
692
|
-
{ name: "delete_graph", description: "Удалить
|
|
892
|
+
{ name: "delete_graph", description: "Удалить граф (DELETE /api/bots/graphs/{id}) — тот же эндпоинт удаляет и вебхук-сценарий (scenarioId из scenario_list/scenario_create — это id того же графа). Активный (опубликованный и назначенный боту) удалить нельзя — будет 409; сначала переключи активный через set_active_graph. Необратимо.", inputSchema: { type: "object", properties: { graphId: { type: "string" } }, required: ["graphId"] } },
|
|
693
893
|
{ name: "set_active_graph", description: "Назначить, какой опубликованный граф активен у бота (переключение живого сценария без перепубликации). HTTP 409 — граф не опубликован или это сценарий-вебхук (источник WEBHOOK — такой включается своей публикацией в вебе).", inputSchema: { type: "object", properties: { botId: { type: "string" }, graphId: { type: "string" } }, required: ["botId", "graphId"] } },
|
|
694
894
|
{ name: "upload_file", description: "Загрузить файл в библиотеку /bots/files (POST /api/bots/media) и получить публичный URL для вставки в сценарий. Передай path (локальный файл) ИЛИ url (перезалить файл по ссылке в своё хранилище). Возвращает {id, url, mediaType, sizeBytes, originalName}. Полученный url ставь в медиа-карточку SEND_MESSAGE (image/video/audio/file/voice/videonote → поле url; gallery → urls[]) или в SEND_PHOTO.photoUrl. Лимит 50 МБ; типы: image/video/audio/pdf/zip/doc(x)/xlsx/pptx/txt (SVG запрещён); при нехватке места — HTTP 402.", inputSchema: { type: "object", properties: { path: { type: "string", description: "Путь к локальному файлу (поддерживается ~)" }, url: { type: "string", description: "Ссылка на файл — будет скачан и перезалит в /bots/files" }, filename: { type: "string", description: "Переопределить имя файла (необязательно)" } } } },
|
|
695
895
|
{ name: "list_files", description: "Список файлов в библиотеке /bots/files (GET /api/bots/media) + использовано/лимит байт. Бери готовые url отсюда, чтобы не загружать одно и то же повторно.", inputSchema: { type: "object", properties: {} } },
|
|
@@ -755,11 +955,14 @@ const TOOLS = [
|
|
|
755
955
|
"text — Telegram-HTML: <b> <i> <u> <s> <code> <pre> <blockquote> <tg-spoiler> <a href=\"https://…\">, перенос строки — \\n (не <br>); прочее экранируется. buttons — до 8 URL-кнопок [{text, url}] (callback-кнопок в рассылке нет). Можно строкой — это TEXT. Медиа — сначала upload_file, потом его url. " +
|
|
756
956
|
"По сценарию (graphId): POST /api/bots/{botId}/broadcasts — один botId, сценарий самого бота, entryNodeId? — с какого узла начать. " +
|
|
757
957
|
"draftId — отправить черновик (поля черновика, переданные аргументы их перекрывают); после отправки черновик удаляется. " +
|
|
758
|
-
"scheduledAt — ISO 8601 (без пояса — московское время); пусто — сразу. Немедленная резервирует квоту получателей тарифа, отложенная считает аудиторию в момент отправки. Рассылки только на платном тарифе — HTTP 402 со ссылкой. У Instagram-ботов рассылок нет.
|
|
759
|
-
|
|
760
|
-
|
|
958
|
+
"scheduledAt — ISO 8601 (без пояса — московское время); пусто — сразу. Немедленная резервирует квоту получателей тарифа, отложенная считает аудиторию в момент отправки. Рассылки только на платном тарифе — HTTP 402 со ссылкой. У Instagram-ботов рассылок нет. " +
|
|
959
|
+
"audienceFilter — полный фильтр аудитории вместо tagsAll/tagsNone (см. свойство ниже; если задан, tagsAll/tagsNone игнорируются). dedup:true — «отправить каждому человеку только один раз» среди нескольких botIds (ключ — platform+userId/chatId приватного чата; человек уходит ПЕРВОМУ боту по порядку botIds, где у него активная сессия). stagger — «распределить по ботам» (см. свойство ниже). Любое из dedup/stagger → ответ вместо {broadcastIds, totalAudience} отдаёт {group:{id, status, perBot[]...}, isGroup:true} — статус/прогресс дальше смотри broadcast_group {action:\"get\", groupId}. Без них — как раньше: {broadcastIds, totalAudience}.",
|
|
960
|
+
inputSchema: { type: "object", properties: { name: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, botId: { type: "string" }, messages: { type: "array", items: {} }, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } }, audienceFilter: AUDIENCE_FILTER_SCHEMA, scheduledAt: { type: "string", description: "ISO 8601, напр. 2026-10-06T10:00:00+03:00; пусто — сразу" }, graphId: { type: "string", description: "рассылка запуском сценария бота вместо сообщений" }, entryNodeId: { type: "string" }, draftId: { type: "string" }, dedup: { type: "boolean", description: "Не отправлять дважды одному человеку среди нескольких botIds (мульти-бот)" }, stagger: STAGGER_SCHEMA } } },
|
|
961
|
+
{ name: "broadcast_cancel", description: "Отменить рассылку (POST /api/bots/broadcasts/{id}/cancel): запланированная не уйдёт, идущая остановится (статус CANCELLING → CANCELLED). Уже завершённую отменить нельзя — HTTP 409. Для группы (dedup/stagger) — broadcast_group {action:\"cancel\"}.", inputSchema: { type: "object", properties: { broadcastId: { type: "string" } }, required: ["broadcastId"] } },
|
|
962
|
+
{ name: "broadcast_recurring", description: "Повторяющиеся рассылки (/api/bots/broadcasts/recurring). action: list — правила (активные и остановленные); create {name, botIds[], messages[], tagsAll?/tagsNone? или audienceFilter?, recurrence: DAILY|MONTHLY|YEARLY, firstRunAt, dedup?, stagger?} — сообщения и фильтр как в broadcast_send; dedup/stagger заданы — КАЖДЫЙ прогон правила порождает свою отдельную группу (своя дедупликация с нуля, аудитория у повтора может быть другой); firstRunAt — первый запуск в будущем (ISO 8601, без пояса — московское), дальше в то же время суток/число (Москва); stop {ruleId} — остановить правило (уже отправленные прогоны не трогаются). Нужен платный тариф (402 со ссылкой).", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "create", "stop"] }, ruleId: { type: "string" }, name: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, botId: { type: "string" }, messages: { type: "array", items: {} }, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } }, audienceFilter: AUDIENCE_FILTER_SCHEMA, recurrence: { type: "string", enum: ["DAILY", "MONTHLY", "YEARLY"] }, firstRunAt: { type: "string" }, dedup: { type: "boolean" }, stagger: STAGGER_SCHEMA }, required: ["action"] } },
|
|
761
963
|
{ 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"] } },
|
|
762
964
|
{ 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" } } } },
|
|
965
|
+
{ name: "broadcast_group", description: "Мульти-бот группа рассылки (dedup/stagger, создаётся broadcast_send/broadcast_recurring). action: preview {botIds[], tagsAll?/tagsNone? или audienceFilter?, dedup?, stagger?} — POST /api/bots/broadcasts/group-preview, read-only: {totalRecipients, uniqueRecipients, perBot:[{botId, username, rawCount, dedupedCount, waves, lastWaveStartAt, plannedStartAt}], limit, scheduleEndsAt}; list — твои группы (новые сверху); get {groupId} — GET .../group/{id}: полная BroadcastGroup {id, status: SCHEDULED|RUNNING|PAUSED|DISPATCHED|CANCELLED, perBot:[{botId, waveIndex, totalWaves, status: PENDING|CLAIMED|DISPATCHED|FAILED|SKIPPED, broadcastId, recipientsRaw, recipientsSkippedDedup, recipientsPlanned, error}]...} — перBot[].broadcastId (после DISPATCHED) смотри прогресс через broadcast_get; pause {groupId} — новые запуски не стартуют, уже идущие (DISPATCHED) боты продолжают слать; resume {groupId} — снимает паузу у просроченных PENDING; cancel {groupId} — PENDING становятся SKIPPED, уже DISPATCHED переводятся в CANCELLING как обычная рассылка. pause/resume/cancel — 409, если группа не в подходящем состоянии; 403 — чужая группа.", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["preview", "list", "get", "pause", "resume", "cancel"] }, groupId: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, tagsAll: { type: "array", items: { type: "string" } }, tagsNone: { type: "array", items: { type: "string" } }, audienceFilter: AUDIENCE_FILTER_SCHEMA, dedup: { type: "boolean" }, stagger: STAGGER_SCHEMA }, required: ["action"] } },
|
|
763
966
|
// ---- Сайты: библиотека шаблонов ----
|
|
764
967
|
// ---- Сайт-виджет, база знаний, подписчик ----
|
|
765
968
|
{ name: "web_widget_snippet", description: "Код вставки чат-виджета на сайт (GET /api/bots/web/{botId}/snippet) — для бота с platform WEB (create_bot {platform:\"WEB\"}). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
@@ -783,13 +986,15 @@ const TOOLS = [
|
|
|
783
986
|
// ---- Запись на слоты (бронирование) ----
|
|
784
987
|
{ name: "booking_calendar_list", description: "Календари записи пользователя (GET /api/bots/booking/calendars): id, name, zone, slotMinutes, hours, exceptions, botId. id — calendarId для действий сценария booking_slots/booking_book/booking_cancel. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
785
988
|
{ name: "booking_calendar_get", description: "Календарь записи по id (GET /api/bots/booking/calendars/{calendarId}). Read-only.", inputSchema: { type: "object", properties: { calendarId: { type: "string" } }, required: ["calendarId"] } },
|
|
786
|
-
{ name: "booking_calendar_create", description: "Создать календарь записи (POST /api/bots/booking/calendars): сетка слотов по рабочим часам в зоне календаря, в слоте — одна бронь. Без hours свободных слотов не будет.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Название (до 100 символов)" }, zone: { type: "string", description: "Часовой пояс IANA, по умолчанию Europe/Moscow" }, slotMinutes: { type: "number", description: "Длительность слота, 5–1440 мин (по умолчанию 60)" }, hours: { type: "array", description: "Рабочие окна: day 1=пн…7=вс, from/to \"HH:mm\"; несколько окон в день — перерыв", items: { type: "object", properties: { day: { type: "number" }, from: { type: "string" }, to: { type: "string" } }, required: ["day", "from", "to"] } }, exceptions: { type: "array", description: "Исключения на даты: {date:\"ГГГГ-ММ-ДД\"} — выходной, с from/to — только это окно", items: { type: "object", properties: { date: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["date"] } }, botId: { type: "string", description: "Необязательно: только для этого бота (иначе — все боты владельца)" } }, required: ["name"] } },
|
|
787
|
-
{ name: "booking_calendar_update", description: "Изменить календарь (PUT /api/bots/booking/calendars/{calendarId}): переданные поля заменяют текущие (hours/exceptions — целиком), остальные сохраняются. Существующие брони не трогаются.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, name: { type: "string", description: "Название (до 100 символов)" }, zone: { type: "string", description: "Часовой пояс IANA, по умолчанию Europe/Moscow" }, slotMinutes: { type: "number", description: "Длительность слота, 5–1440 мин (по умолчанию 60)" }, hours: { type: "array", description: "Рабочие окна: day 1=пн…7=вс, from/to \"HH:mm\"; несколько окон в день — перерыв", items: { type: "object", properties: { day: { type: "number" }, from: { type: "string" }, to: { type: "string" } }, required: ["day", "from", "to"] } }, exceptions: { type: "array", description: "Исключения на даты: {date:\"ГГГГ-ММ-ДД\"} — выходной, с from/to — только это окно", items: { type: "object", properties: { date: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["date"] } }, botId: { type: "string", description: "Необязательно: только для этого бота (иначе — все боты владельца)" } }, required: ["calendarId"] } },
|
|
989
|
+
{ name: "booking_calendar_create", description: "Создать календарь записи (POST /api/bots/booking/calendars): сетка слотов по рабочим часам в зоне календаря, в слоте — одна бронь. Без hours свободных слотов не будет. Доп. поля (буферы, горизонт, типы встреч, форма клиента, подтверждение владельцем, публичная ссылка /book/<slug>) — см. свойства ниже.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Название (до 100 символов)" }, zone: { type: "string", description: "Часовой пояс IANA, по умолчанию Europe/Moscow" }, slotMinutes: { type: "number", description: "Длительность слота, 5–1440 мин (по умолчанию 60)" }, hours: { type: "array", description: "Рабочие окна: day 1=пн…7=вс, from/to \"HH:mm\"; несколько окон в день — перерыв", items: { type: "object", properties: { day: { type: "number" }, from: { type: "string" }, to: { type: "string" } }, required: ["day", "from", "to"] } }, exceptions: { type: "array", description: "Исключения на даты: {date:\"ГГГГ-ММ-ДД\"} — выходной, с from/to — только это окно", items: { type: "object", properties: { date: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["date"] } }, botId: { type: "string", description: "Необязательно: только для этого бота (иначе — все боты владельца)" }, ...BOOKING_CAL_EXTRA_PROPS }, required: ["name"] } },
|
|
990
|
+
{ name: "booking_calendar_update", description: "Изменить календарь (PUT /api/bots/booking/calendars/{calendarId}): переданные поля заменяют текущие (hours/exceptions/breaks/meetingTypes/intakeFields — целиком), остальные сохраняются. Существующие брони не трогаются.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, name: { type: "string", description: "Название (до 100 символов)" }, zone: { type: "string", description: "Часовой пояс IANA, по умолчанию Europe/Moscow" }, slotMinutes: { type: "number", description: "Длительность слота, 5–1440 мин (по умолчанию 60)" }, hours: { type: "array", description: "Рабочие окна: day 1=пн…7=вс, from/to \"HH:mm\"; несколько окон в день — перерыв", items: { type: "object", properties: { day: { type: "number" }, from: { type: "string" }, to: { type: "string" } }, required: ["day", "from", "to"] } }, exceptions: { type: "array", description: "Исключения на даты: {date:\"ГГГГ-ММ-ДД\"} — выходной, с from/to — только это окно", items: { type: "object", properties: { date: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["date"] } }, botId: { type: "string", description: "Необязательно: только для этого бота (иначе — все боты владельца)" }, ...BOOKING_CAL_EXTRA_PROPS }, required: ["calendarId"] } },
|
|
788
991
|
{ name: "booking_calendar_delete", description: "УДАЛИТЬ календарь записи (DELETE /api/bots/booking/calendars/{calendarId}). Сценарии с его calendarId перестанут записывать. Только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, confirm: { type: "boolean" } }, required: ["calendarId", "confirm"] } },
|
|
789
|
-
{ name: "booking_slots", description: "Свободные слоты календаря (GET /api/bots/booking/calendars/{calendarId}/slots): [{at — ISO-8601 с зоной, label — «пн 12.10 15:00»}]. from/to — даты ГГГГ-ММ-ДД в зоне календаря (по умолчанию сегодня … +6 дней), limit — 1–500 (по умолчанию 50). Read-only.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, from: { type: "string" }, to: { type: "string" }, limit: { type: "number" } }, required: ["calendarId"] } },
|
|
790
|
-
{ name: "booking_list", description: "Брони календаря от даты from (ГГГГ-ММ-ДД, по умолчанию сегодня), включая отменённые, до 500 (GET /api/bots/booking/calendars/{calendarId}/bookings). Read-only.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, from: { type: "string" } }, required: ["calendarId"] } },
|
|
791
|
-
{ name: "booking_create", description: "Записать вручную на слот (POST /api/bots/booking/calendars/{calendarId}/bookings
|
|
992
|
+
{ name: "booking_slots", description: "Свободные слоты календаря (GET /api/bots/booking/calendars/{calendarId}/slots): [{at — ISO-8601 с зоной, label — «пн 12.10 15:00»}]. from/to — даты ГГГГ-ММ-ДД в зоне календаря (по умолчанию сегодня … +6 дней), limit — 1–500 (по умолчанию 50). meetingTypeId — у календаря с типами встреч выбрать нужный (меняет шаг сетки); неизвестный id — как без него (slotMinutes). Read-only.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, from: { type: "string" }, to: { type: "string" }, limit: { type: "number" }, meetingTypeId: { type: "string", description: "id из meetingTypes календаря" } }, required: ["calendarId"] } },
|
|
993
|
+
{ name: "booking_list", description: "Брони календаря от даты from (ГГГГ-ММ-ДД, по умолчанию сегодня), включая отменённые, до 500 (GET /api/bots/booking/calendars/{calendarId}/bookings). pendingOnly:true — игнорирует from, только брони MANUAL-календаря, ждущие confirm/reject (без потолка 500). Read-only.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, from: { type: "string" }, pendingOnly: { type: "boolean" } }, required: ["calendarId"] } },
|
|
994
|
+
{ name: "booking_create", description: "Записать вручную на слот (POST /api/bots/booking/calendars/{calendarId}/bookings): slotAt — at из booking_slots (ISO-8601 с зоной). Слот занят → понятная ошибка «занят», слота нет в сетке → ошибка 400. email/comment/customAnswers — доп. поля формы клиента (intakeFields календаря — только подсказка для UI, не ограничивает, что можно передать здесь); meetingTypeId — у календаря с типами встреч. MANUAL confirmationMode — бронь создаётся с confirmed:false, слот уже занят; подтвердить/отклонить — booking_confirm/booking_reject.", inputSchema: { type: "object", properties: { calendarId: { type: "string" }, slotAt: { type: "string" }, name: { type: "string" }, phone: { type: "string" }, email: { type: "string" }, comment: { type: "string" }, customAnswers: { type: "object", additionalProperties: { type: "string" }, description: "Доп. поля формы: {key: значение}, ключи — intakeFields календаря" }, meetingTypeId: { type: "string" } }, required: ["calendarId", "slotAt"] } },
|
|
792
995
|
{ 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"] } },
|
|
996
|
+
{ 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"] } },
|
|
997
|
+
{ 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"] } },
|
|
793
998
|
{ 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" } } } },
|
|
794
999
|
// ---- Паритет платформы, часть 2: бот, база, агент, заявки, подписчики, каналы, ссылки, аналитика ----
|
|
795
1000
|
{ 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"] } },
|
|
@@ -809,6 +1014,58 @@ const TOOLS = [
|
|
|
809
1014
|
{ name: "site_leads_export", description: "Все заявки сайта в CSV (GET /api/bots/pages/{siteId}/leads.csv). savePath — сохранить в локальный файл и вернуть путь (для больших выгрузок); без него CSV возвращается текстом. Read-only.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, savePath: { type: "string" } }, required: ["siteId"] } },
|
|
810
1015
|
{ name: "bot_users_export", description: "Выгрузка всех подписчиков бота (GET /api/bots/{botId}/users/export?format=csv|json): chatId, имя, метки, переменные. savePath — сохранить в локальный файл и вернуть путь; без него данные возвращаются текстом. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, format: { type: "string", enum: ["csv", "json"] }, savePath: { type: "string" } }, required: ["botId"] } },
|
|
811
1016
|
{ name: "bot_user_reset", description: "Сбросить сессию подписчика (DELETE /api/bots/{botId}/sessions/{chatId}): стираются его позиция в сценарии, метки и переменные, и он пропадает из list_bot_users; при следующем сообщении начнёт как новый. Для повторного прохождения воронки при тесте. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, confirm: { type: "boolean" } }, required: ["botId", "chatId", "confirm"] } },
|
|
1017
|
+
// ---- Аудитория (кросс-бот поиск/фильтр подписчиков, теги/переменные массово, экспорт) ----
|
|
1018
|
+
{ name: "audience_fields", description: "Поля для построителя фильтра (GET /api/bots/{botId}/audience/fields): variables — [{key, type: TEXT|NUMBER|DATE|BOOL}] (тип выведен по значению в сэмпле из последних 500 сессий — не точная схема), tags — список тегов. search — подстрока (и по ключам переменных, и по тегам), limit — до 200 каждого списка. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, search: { type: "string" }, limit: { type: "number" } }, required: ["botId"] } },
|
|
1019
|
+
{ name: "audience_search", description: "Фильтр+сортировка+поиск подписчиков (замена постраничного перебора list_bot_users под сложный фильтр). botId — один бот (POST /api/bots/{botId}/users/search); без botId — кросс-бот по всем доступным (POST /api/bots/subscribers/search, опц. botIds[] — подмножество, чужие id тихо отбрасываются). filter — AudienceFilter (см. свойство ниже), q — текстовый поиск (имя/username/id), sort ∈ updatedAt|activity|lastActivity|chatId|username|tgUsername|createdAt|subscribedAt|status|blocked|botId|bot, dir — asc|desc, page (с 0), size (до 200, по умолч. 25). Кросс-бот ответ — те же поля + botId/botName. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, filter: AUDIENCE_FILTER_SCHEMA, q: { type: "string" }, sort: { type: "string" }, dir: { type: "string", enum: ["asc", "desc"] }, page: { type: "number" }, size: { type: "number" } } } },
|
|
1020
|
+
{ name: "audience_tag_edit", description: "Массово добавить/снять теги у выбранных подписчиков бота (POST /api/bots/{botId}/users/tags): chatIds[] (до 10000, из audience_search/list_bot_users), add?/remove? — списки тегов; remove применяется ДО add (тег в обоих спискaх — останется). Возвращает {matched}.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatIds: { type: "array", items: { type: "string" } }, add: { type: "array", items: { type: "string" } }, remove: { type: "array", items: { type: "string" } } }, required: ["botId", "chatIds"] } },
|
|
1021
|
+
{ name: "audience_variable_edit", description: "Массово установить/снять custom-переменные у выбранных подписчиков (POST /api/bots/{botId}/users/variables): chatIds[] (до 1000 — читается документ целиком, потолок ниже, чем у audience_tag_edit), set? — {ключ: значение}, unset? — список ключей. Служебные ключи и ключи с «.»/ведущим «$» отклоняются (400). Каждая запись проходит тот же потолок переменных подписчика (одно значение ≤65536, сумма ≤262144 символов) — отклонённые видны в ответе {matched, rejected[] (до 50 по имени), rejectedCount}, остальные записи применяются как обычно.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatIds: { type: "array", items: { type: "string" } }, set: { type: "object" }, unset: { type: "array", items: { type: "string" } } }, required: ["botId", "chatIds"] } },
|
|
1022
|
+
{ name: "audience_export", description: "Потоковая выгрузка ТЕКУЩЕЙ отфильтрованной выборки подписчиков (не только одной страницы) — CSV или XLSX. botId — один бот (POST /api/bots/{botId}/users/export); без botId — кросс-бот (POST /api/bots/subscribers/export, опц. botIds[], доп. колонка «бот»). filter — AudienceFilter, q — текстовый поиск. Потолок 100000 строк — больше → HTTP 422 (сузь фильтр). format csv — текст (savePath необязателен, иначе вернётся строкой); format xlsx — ВСЕГДА требует savePath (бинарный формат, в чат не возвращается).", inputSchema: { type: "object", properties: { botId: { type: "string" }, botIds: { type: "array", items: { type: "string" } }, filter: AUDIENCE_FILTER_SCHEMA, q: { type: "string" }, format: { type: "string", enum: ["csv", "xlsx"] }, savePath: { type: "string", description: "Обязательно для xlsx; для csv — необязательно (иначе выгрузка возвращается текстом)" } } } },
|
|
1023
|
+
{ name: "dialog_search", description: "Кросс-бот поиск по «Диалогам» (GET /api/bots/dialogs/search): q ищет по имени/username/id подписчика, названию бота И тексту сообщений (а не только карточке подписчика, в отличие от list_bot_users). limit — 1–60 (по умолчанию 40), объединённые результаты, свежие сверху, не постраничный — для большой выгрузки используй audience_search/audience_export. Только боты, доступные тебе. Read-only.", inputSchema: { type: "object", properties: { q: { type: "string" }, limit: { type: "number" } }, required: ["q"] } },
|
|
1024
|
+
// ---- Публикации: квота и VK ----
|
|
1025
|
+
{ name: "publications_usage", description: "Месячная квота публикаций (GET /api/bots/{botId}/publications/usage) — ОДНА на владельца бота, общая для всех его ботов и платформ (Telegram/MAX channel_post, Instagram, VK — любой публикующий путь списывает её же). {used, limit, unlimited, planCode}; limit=-1/unlimited:true — без лимита. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" } }, required: ["botId"] } },
|
|
1026
|
+
{ name: "vk_status", description: "Подключённые VK-сообщества/страницы аккаунта (GET /api/bots/vk/status) — подключение АККАУНТА, не бота: видно во всех ботах владельца. {connected, accounts: [{id, platform:\"vk\", label, enabled, isDefault, hasCredentials}]}. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1027
|
+
{ name: "vk_connect", description: "Подключить VK-сообщество вручную токеном (POST /api/bots/vk/connect-token): communityId, accessToken — токен сообщества с правами wall (и photos, если нужны фото-посты). Токен проверяется живым вызовом VK API перед сохранением — невалидный/без прав → HTTP 422 с причиной от VK, токен не сохраняется и не эхуется. Альтернатива — вход через VK ID (OAuth) в кабинете «Публикации»: по API недоступен, это браузерный флоу.", inputSchema: { type: "object", properties: { communityId: { type: "string" }, accessToken: { type: "string" } }, required: ["communityId", "accessToken"] } },
|
|
1028
|
+
{ name: "vk_disconnect", description: "Отключить VK-сообщество/страницу (DELETE /api/bots/vk/{accountId}); accountId — из vk_status. 404 — не найден/чужой.", inputSchema: { type: "object", properties: { accountId: { type: "string" } }, required: ["accountId"] } },
|
|
1029
|
+
{ name: "vk_post", description: "Опубликовать пост на стену подключённого VK-сообщества/страницы (POST /api/bots/vk/post): accountId (vk_status), text и/или mediaUrl (ТОЛЬКО фото — JPEG/PNG из upload_file; видео/другие типы отклоняются HTTP 422). Списывает publications_usage (резерв до отправки, откат при провале VK). HTTP 402 — квота исчерпана; 422 — VK отклонил (текст ошибки VK как есть).", inputSchema: { type: "object", properties: { accountId: { type: "string" }, text: { type: "string" }, mediaUrl: { type: "string", description: "url из upload_file — только изображение" } }, required: ["accountId"] } },
|
|
1030
|
+
// ---- Миграция подписчиков из других конструкторов ----
|
|
1031
|
+
{ name: "migration_presets", description: "Каталог поддерживаемых источников миграции (GET /api/bots/migration/presets): BOTHELP|SALEBOT|SENLER|LEADTEH|SMARTSENDER|MANYCHAT|GENERIC — у каждого note (как экспортировать оттуда) и guessed*Headers (подсказки для автосопоставления колонок). Переносятся ТОЛЬКО подписчики (funnelImportSupported всегда false — ни один из этих билдеров не публикует документированный формат экспорта сценария; свои воронки переносятся через import_funnel/create_graph с retensy-bot-graph JSON, это другой формат). Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1032
|
+
{ name: "migration_preview", description: "Пробный прогон CSV-импорта подписчиков, НИЧЕГО не пишет в базу (POST /api/bots/{botId}/migration/subscribers/preview, multipart). path — локальный CSV (≤10 МБ, ≤20000 строк данных), platform — id из migration_presets. Без mapping — режим определения: headers, sampleRaw (до 5 строк), suggestedMapping (угадан по подсказкам пресета — только подсказка, не контракт). С mapping ({userIdColumn (обязательно — заголовок колонки с числовым chatId), usernameColumn?, tagsColumn?, tagsDelimiter? (по умолч. \"|;,\"), variableColumns?}) — полный dry-run: validRows, invalidRows (причины, до 20), newSubscribers/existingSubscribers (дедуп по chatId против текущих подписчиков бота), overCapacity (сверх лимита тарифа), remainingCapacity (-1 — безлимит), sample (до 10 строк как они импортируются), totalRowsInFile/truncated.", inputSchema: { type: "object", properties: { botId: { type: "string" }, path: { type: "string", description: "Локальный путь к CSV-файлу" }, platform: { type: "string", enum: ["BOTHELP", "SALEBOT", "SENLER", "LEADTEH", "SMARTSENDER", "MANYCHAT", "GENERIC"] }, mapping: { type: "object", description: "MigrationColumnMapping — передай ПОСЛЕ того, как проверил suggestedMapping/detect-режимом", properties: { userIdColumn: { type: "string" }, usernameColumn: { type: "string" }, tagsColumn: { type: "string" }, tagsDelimiter: { type: "string" }, variableColumns: { type: "array", items: { type: "string" } } } } }, required: ["botId", "path", "platform"] } },
|
|
1033
|
+
{ name: "migration_commit", description: "Реальный импорт подписчиков из CSV тем же mapping, что подтвердил migration_preview (POST /api/bots/{botId}/migration/subscribers/commit, multipart) — тот же путь дедупа по chatId/лимита тарифа/«никогда не перезаписывать существующее», что у bot_users_import. Возвращает {totalRowsInFile, processedRows, truncated, result: ImportUsersResponse}.", inputSchema: { type: "object", properties: { botId: { type: "string" }, path: { type: "string" }, platform: { type: "string", enum: ["BOTHELP", "SALEBOT", "SENLER", "LEADTEH", "SMARTSENDER", "MANYCHAT", "GENERIC"] }, mapping: { type: "object", properties: { userIdColumn: { type: "string" }, usernameColumn: { type: "string" }, tagsColumn: { type: "string" }, tagsDelimiter: { type: "string" }, variableColumns: { type: "array", items: { type: "string" } } }, required: ["userIdColumn"] } }, required: ["botId", "path", "platform", "mapping"] } },
|
|
1034
|
+
// ---- Публикации: цепочки (автопостинг «Цепочки публикаций») ----
|
|
1035
|
+
{ name: "chain_list", description: "Цепочки публикаций пользователя (GET /api/bots/chains): автоматический репост source → destinations[] с шагами обработки (steps[]) и UTM. Цепочка привязана к владельцу, не к конкретному боту. Read-only.", inputSchema: { type: "object", properties: {} } },
|
|
1036
|
+
{ name: "chain_get", description: "Цепочка по id (GET /api/bots/chains/{id}). 404 — не своя/не найдена. Read-only.", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1037
|
+
{ name: "chain_create", description: "Создать цепочку публикаций (POST /api/bots/chains): chain — {name, enabled?, approvalMode?: NONE|AI_ONLY|ALL, source: {type: TELEGRAM|VK|RSS, botId?, chatId?, accountId?, rssUrl?}, destinations: [{type: TELEGRAM|MAX|VK|INSTAGRAM, botId?, chatId?, accountId?, utmOverride?}], steps?: [{type: FILTER|TEXT_TRANSFORM|AI_ADAPT|UTM|DELAY, ...поля шага}], utmDefaults?}. TELEGRAM-источник — свой канал бота (bot_channel_list/list_channels), VK-источник/назначение — подключённый аккаунт (vk_status). INSTAGRAM в destinations принимается моделью, но публикация требует медиа и подключённый Instagram-аккаунт — без них задание уходит в SKIPPED с причиной, не FAILED. HTTP 400 — не хватает полей или источник/назначение не найдены/не ваши; HTTP 409 — прямой цикл с другой вашей цепочкой (A→B при уже существующей B→A).", inputSchema: { type: "object", properties: { chain: { type: "object", description: "PublishChain draft — см. описание инструмента" } }, required: ["chain"] } },
|
|
1038
|
+
{ name: "chain_update", description: "Изменить цепочку целиком (PUT /api/bots/chains/{id}): chain — PublishChain draft (та же форма, что в chain_create), заменяет текущую конфигурацию полностью — сначала прочитай chain_get и поменяй нужное. Те же 400/409, что у chain_create.", inputSchema: { type: "object", properties: { chainId: { type: "string" }, chain: { type: "object", description: "PublishChain draft целиком" } }, required: ["chainId", "chain"] } },
|
|
1039
|
+
{ name: "chain_delete", description: "УДАЛИТЬ цепочку публикаций (DELETE /api/bots/chains/{id}). История заданий (chain_runs) теряется вместе с ней. Необратимо: только по явной просьбе, с confirm:true.", inputSchema: { type: "object", properties: { chainId: { type: "string" }, confirm: { type: "boolean" } }, required: ["chainId", "confirm"] } },
|
|
1040
|
+
{ name: "chain_enable", description: "Включить цепочку (POST /api/bots/chains/{id}/enable) — начинает принимать новые посты источника (TELEGRAM — сразу по событию, VK/RSS — со следующего опроса, раз в 3 мин).", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1041
|
+
{ name: "chain_pause", description: "Поставить цепочку на паузу (POST /api/bots/chains/{id}/pause) — перестаёт принимать новые посты источника (VK/RSS — опрос останавливается, TELEGRAM — события игнорируются); история и настройки сохраняются.", inputSchema: { type: "object", properties: { chainId: { type: "string" } }, required: ["chainId"] } },
|
|
1042
|
+
{ name: "chain_runs", description: "История заданий цепочки (GET /api/bots/chains/{id}/runs), новые сверху: [{id, status: PENDING|RUNNING|DONE|FAILED|AWAITING_APPROVAL|SKIPPED, originId, destinationId, lastError, reviewedBy, reviewedAt…}]. Бэкенд отдаёт всю историю без постраничной выдачи на сервере — page (с 0)/size (по умолчанию 20) режут список уже здесь, в MCP, чтобы не упираться в лимит токенов на цепочке с долгой историей; без них — список целиком. Read-only.", inputSchema: { type: "object", properties: { chainId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["chainId"] } },
|
|
1043
|
+
{ name: "chain_approve", description: "Согласовать задание, ждущее подтверждения (approvalMode AI_ONLY/ALL) — POST /api/bots/chains/jobs/{jobId}/approve: AWAITING_APPROVAL → PENDING, подхватится ближайшим тиком исполнения (раз в 15 сек). jobId — из chain_runs. HTTP 400 — задание сейчас не ждёт согласования.", inputSchema: { type: "object", properties: { jobId: { type: "string" } }, required: ["jobId"] } },
|
|
1044
|
+
{ name: "chain_reject", description: "Отклонить задание, ждущее подтверждения — POST /api/bots/chains/jobs/{jobId}/reject: AWAITING_APPROVAL → SKIPPED, публикации не будет. jobId — из chain_runs. HTTP 400 — задание сейчас не ждёт согласования.", inputSchema: { type: "object", properties: { jobId: { type: "string" } }, required: ["jobId"] } },
|
|
1045
|
+
{ name: "chain_retry", description: "Повторить проваленное задание вручную, минуя бэкофф (POST /api/bots/chains/jobs/{jobId}/retry): сбрасывает lastError, задание подхватится ближайшим тиком исполнения. jobId — из chain_runs (status FAILED).", inputSchema: { type: "object", properties: { jobId: { type: "string" } }, required: ["jobId"] } },
|
|
1046
|
+
// ---- Аналитика (дашборд «/bots/analytics»: сайт/бот/сценарий/рассылка) ----
|
|
1047
|
+
{ name: "site_analytics", description: "KPI+тренд сайта (GET /api/bots/pages/{siteId}/analytics?from&to): {pageViews, clicks, formSubmits, uniqueVisitors, leads, topPages, topReferrers, utmSources, trend:[{day,pageViews,clicks,formSubmits,uniqueVisitors}]}. from/to — ГГГГ-ММ-ДД, по умолчанию последние 30 дней UTC. uniqueVisitors — сумма дневных уников (не дедуп по всему периоду, это приближение). clicks считает только элементы с goalName, заданным в редакторе сайта (data-rs-track), а не любой клик. Read-only.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, from: { type: "string", description: "ГГГГ-ММ-ДД" }, to: { type: "string", description: "ГГГГ-ММ-ДД" } }, required: ["siteId"] } },
|
|
1048
|
+
{ name: "site_analytics_events", description: "Сырые события аналитики сайта постранично (GET /api/bots/pages/{siteId}/analytics/events?from&to&page&size): {items:[{id, type: PAGE_VIEW|CLICK|FORM_SUBMIT, objectId, visitorId, page, referrer, utmSource/Medium/Campaign/Content/Term, props, createdAt}], total}. from/to — ГГГГ-ММ-ДД (по умолчанию последние 30 дней), page (с 0), size (по умолчанию 50). Read-only.", inputSchema: { type: "object", properties: { siteId: { type: "string" }, from: { type: "string" }, to: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["siteId"] } },
|
|
1049
|
+
{ name: "bot_subscriber_analytics", description: "Новые подписчики/активность бота по дням (GET /api/bots/{botId}/analytics/subscribers?from&to): {newSubscribers, active, total, legacyExcluded, trend:[{day,newSubscribers,active}]}. from/to обязательны — ГГГГ-ММ-ДД, диапазон обрезается сервером до 92 дней. legacyExcluded — сессии без даты создания (заведены до этой фичи): не входят в newSubscribers/trend, но учтены в total. active — по updatedAt в диапазоне (одна сессия может посчитаться в несколько дней). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, from: { type: "string", description: "ГГГГ-ММ-ДД" }, to: { type: "string", description: "ГГГГ-ММ-ДД" } }, required: ["botId", "from", "to"] } },
|
|
1050
|
+
{ name: "scenario_timeseries", description: "Дневной ряд прогонов сценария (GET /api/bots/graphs/{graphId}/analytics/timeseries?from&to): [{day, ok, failed, other}] — по одной строке на каждый день диапазона (пустые дни — нулями), other = PARKED/NO_MATCH/PARTIAL (не отброшены). from/to обязательны — ГГГГ-ММ-ДД, диапазон обрезается до 92 дней. Разбивку по узлам сценария (не по дням) даёт graph_analytics — это разные срезы одних данных. Read-only.", inputSchema: { type: "object", properties: { graphId: { type: "string" }, from: { type: "string", description: "ГГГГ-ММ-ДД" }, to: { type: "string", description: "ГГГГ-ММ-ДД" } }, required: ["graphId", "from", "to"] } },
|
|
1051
|
+
// ---- ИИ-агенты: инструменты (вызов внешних систем моделью) ----
|
|
1052
|
+
{ name: "agent_tool_test", description: "Проверить инструмент агента напрямую, БЕЗ модели (POST /api/bots/agents/{agentId}/tools/{toolId}/test): params — аргументы вызова (AMO_LOOKUP: query ИЛИ leadId; SHEETS_LOOKUP: value; HTTP_API: имена из httpParams этого инструмента). Проверяет только конфиг инструмента (подключение amoCRM/таблицу Sheets/URL) — модель не вызывается, бюджет ИИ не тратится. {ok:true, result} при успехе; ошибка конфига/внешнего сервиса — HTTP 400 {ok:false, error} с причиной на русском (никогда не 500). toolId — id инструмента из agent_get → tools[].id (настраивается через agent_update {patch:{tools:[...]}})." , inputSchema: { type: "object", properties: { agentId: { type: "string" }, toolId: { type: "string" }, params: { type: "object", description: "Аргументы вызова инструмента (см. описание)" } }, required: ["agentId", "toolId"] } },
|
|
1053
|
+
// ---- Заявки сайта: файлы вложений ----
|
|
1054
|
+
{ 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"] } },
|
|
1055
|
+
// ---- Вебхук-сценарии (/api/scenarios, PAT-доступ открыт решением владельца 14.10) ----
|
|
1056
|
+
{ 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: {} } },
|
|
1057
|
+
{ 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"] } },
|
|
1058
|
+
{ 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"] } },
|
|
1059
|
+
{ 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"] } },
|
|
1060
|
+
{ 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"] } },
|
|
1061
|
+
{ name: "scenario_rotate_secret", description: "Выпустить новый секрет входа вебхук-сценария (POST /api/scenarios/{id}/rotate-secret) — старый сразу перестаёт приниматься. Секрет виден в ответе этого вызова и в scenario_get (source.config.secret) — это собственный секрет сценария для настройки его входящего вебхука, не чужой секрет интеграции.", inputSchema: { type: "object", properties: { scenarioId: { type: "string" } }, required: ["scenarioId"] } },
|
|
1062
|
+
// ---- Публичный API ИИ-агента: ключи (решение владельца 14.10) ----
|
|
1063
|
+
{ name: "agent_api_key_create", description: "Выпустить API-ключ публичного чата агента (POST /api/bots/agents/{agentId}/api-keys): один ключ работает только с ЭТИМ агентом (не аккаунтом целиком, в отличие от zmcp-токена). Секрет (вид zagt_...) виден ТОЛЬКО в этом ответе — дальше хранится лишь хеш; сохрани/покажи пользователю сразу, повторно его не получить. До 20 активных ключей на агента — сверх лимита HTTP 409 TOO_MANY_KEYS, сначала отзови лишний (agent_api_key_revoke). Ключ шлют заголовком Authorization: Bearer <ключ> на POST /api/public/agents/{agentId}/chat (публичный чат агента, без zmcp-токена/сессии) — {conversationId?, message (1–1000 симв.), metadata?} → {reply, conversationId, sources?}; 30 запросов/мин на ключ (429 иначе), агент должен быть опубликован (agent_publish).", inputSchema: { type: "object", properties: { agentId: { type: "string" }, name: { type: "string", description: "Название ключа для списка, например «Виджет на сайте»" } }, required: ["agentId"] } },
|
|
1064
|
+
{ name: "agent_api_key_list", description: "Список API-ключей агента (GET /api/bots/agents/{agentId}/api-keys): {id, name, keyPrefix, createdAt, lastUsedAt} — без секрета (key:null), секрет виден только один раз при agent_api_key_create. Read-only.", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
|
|
1065
|
+
{ name: "agent_api_key_revoke", description: "Отозвать API-ключ агента (DELETE /api/bots/agents/{agentId}/api-keys/{keyId}) — публичный чат с этим ключом сразу перестаёт отвечать (401). keyId — из agent_api_key_list. Необратимо: только по явной просьбе пользователя, с confirm:true.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, keyId: { type: "string" }, confirm: { type: "boolean" } }, required: ["agentId", "keyId", "confirm"] } },
|
|
1066
|
+
// ---- Пакеты ответов ИИ сверх тарифа (решение владельца 14.10) ----
|
|
1067
|
+
{ name: "ai_packs", description: "Пакеты ответов ИИ сверх месячного бюджета тарифа: каталог (GET /api/bots/ai-packs) — [{sku, answers, priceRub, kopecks}] (ai_pack_1000 — 1000 ответов/490₽, ai_pack_5000 — 5000/1990₽, ai_pack_20000 — 20000/6990₽; kopecks — внутренняя единица бюджета, справочно, не присылай её). Плюс баланс — aiPackAnswersLeft/aiPackTokensLeft из GET /api/bots/subscription: пакет не сгорает без активного тарифа, но списывается только при нём, ПОСЛЕ месячного бюджета тарифа, FIFO по старшинству покупки (сначала самый старый). Read-only; купить — ai_pack_purchase.", inputSchema: { type: "object", properties: {} } },
|
|
1068
|
+
{ name: "ai_pack_purchase", description: "ЭТО ПЛАТЁЖ, не покупка напрямую. Купить пакет ответов ИИ (POST /api/bots/ai-packs/purchase {sku, method}): тот же конвейер оплаты (ЮKassa/крипта), что у смены тарифа — ответ НЕ списывает деньги сам, а возвращает invoiceUrl, ссылку на оплату; пакет зачислится на баланс только после подтверждения платежа пользователем в браузере. sku — из ai_packs; method — \"card\" (по умолчанию), как у subscribe. Только по явной просьбе пользователя, с confirm:true.", inputSchema: { type: "object", properties: { sku: { type: "string", description: "sku из ai_packs, например ai_pack_5000" }, method: { type: "string", description: "card (по умолчанию) и т.п., как у subscribe" }, confirm: { type: "boolean" } }, required: ["sku", "confirm"] } },
|
|
812
1069
|
];
|
|
813
1070
|
|
|
814
1071
|
async function handleCall(params) {
|
|
@@ -1182,10 +1439,16 @@ async function handleCall(params) {
|
|
|
1182
1439
|
if (!Number.isInteger(n) || n < 1 || n > 500) throw new Error("limit — целое от 1 до 500.");
|
|
1183
1440
|
q.limit = n;
|
|
1184
1441
|
}
|
|
1442
|
+
if (a.meetingTypeId) q.meetingTypeId = uuidArg(a.meetingTypeId, "meetingTypeId");
|
|
1185
1443
|
return okResult(await api(`/api/bots/booking/calendars/${uuidArg(a.calendarId, "calendarId")}/slots${qs(q)}`));
|
|
1186
1444
|
}
|
|
1187
1445
|
case "booking_list": {
|
|
1188
|
-
const q =
|
|
1446
|
+
const q = {};
|
|
1447
|
+
if (a.from) q.from = dateArg(a.from, "from");
|
|
1448
|
+
if (a.pendingOnly != null) {
|
|
1449
|
+
if (typeof a.pendingOnly !== "boolean") throw new Error("pendingOnly — true или false.");
|
|
1450
|
+
q.pendingOnly = a.pendingOnly;
|
|
1451
|
+
}
|
|
1189
1452
|
return okResult(await api(`/api/bots/booking/calendars/${uuidArg(a.calendarId, "calendarId")}/bookings${qs(q)}`));
|
|
1190
1453
|
}
|
|
1191
1454
|
case "booking_create": {
|
|
@@ -1194,8 +1457,13 @@ async function handleCall(params) {
|
|
|
1194
1457
|
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.test(slotAt) || Number.isNaN(Date.parse(slotAt))) {
|
|
1195
1458
|
throw new Error(`slotAt — ISO-8601 с зоной (at из booking_slots), например 2026-10-12T15:00:00+03:00; получено: ${slotAt || "пусто"}.`);
|
|
1196
1459
|
}
|
|
1460
|
+
const body = { slotAt, name: a.name, phone: a.phone };
|
|
1461
|
+
if (a.email !== undefined) body.email = a.email;
|
|
1462
|
+
if (a.comment !== undefined) body.comment = a.comment;
|
|
1463
|
+
if (a.customAnswers !== undefined) body.customAnswers = a.customAnswers;
|
|
1464
|
+
if (a.meetingTypeId) body.meetingTypeId = uuidArg(a.meetingTypeId, "meetingTypeId");
|
|
1197
1465
|
try {
|
|
1198
|
-
return okResult(await api(`/api/bots/booking/calendars/${id}/bookings`, { method: "POST", body
|
|
1466
|
+
return okResult(await api(`/api/bots/booking/calendars/${id}/bookings`, { method: "POST", body }));
|
|
1199
1467
|
} catch (e) {
|
|
1200
1468
|
if (e.status === 409) throw new Error(`Слот ${slotAt} уже занят — выбери другой из booking_slots.`);
|
|
1201
1469
|
throw e;
|
|
@@ -1207,6 +1475,20 @@ async function handleCall(params) {
|
|
|
1207
1475
|
await api(`/api/bots/booking/calendars/${id}/bookings/${bookingId}/cancel`, { method: "POST" });
|
|
1208
1476
|
return okResult(`✅ Бронь ${bookingId} отменена, слот свободен.`);
|
|
1209
1477
|
}
|
|
1478
|
+
case "booking_confirm":
|
|
1479
|
+
case "booking_reject": {
|
|
1480
|
+
const id = uuidArg(a.calendarId, "calendarId");
|
|
1481
|
+
const bookingId = uuidArg(a.bookingId, "bookingId");
|
|
1482
|
+
const verb = params.name === "booking_confirm" ? "confirm" : "reject";
|
|
1483
|
+
try {
|
|
1484
|
+
await api(`/api/bots/booking/calendars/${id}/bookings/${bookingId}/${verb}`, { method: "POST" });
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
if (e.status === 404) throw new Error(`Бронь ${bookingId} не найдена в календаре ${id}.`);
|
|
1487
|
+
if (e.status === 409) throw new Error(`Бронь ${bookingId} уже не ждёт решения (подтверждена, отменена или истёк срок ожидания).`);
|
|
1488
|
+
throw e;
|
|
1489
|
+
}
|
|
1490
|
+
return okResult(verb === "confirm" ? `✅ Бронь ${bookingId} подтверждена.` : `❌ Бронь ${bookingId} отклонена, слот свободен.`);
|
|
1491
|
+
}
|
|
1210
1492
|
// ---- Паритет платформы ----
|
|
1211
1493
|
case "integration_calls": {
|
|
1212
1494
|
const q = {};
|
|
@@ -1391,6 +1673,101 @@ async function handleCall(params) {
|
|
|
1391
1673
|
await api(`/api/bots/${botId}/sessions/${chatId}`, { method: "DELETE" });
|
|
1392
1674
|
return okResult(`♻️ Сессия подписчика ${chatId} сброшена: при следующем сообщении он начнёт как новый.`);
|
|
1393
1675
|
}
|
|
1676
|
+
// ---- Аудитория ----
|
|
1677
|
+
case "audience_fields": {
|
|
1678
|
+
const botId = uuidArg(a.botId, "botId");
|
|
1679
|
+
const q = {};
|
|
1680
|
+
if (a.search) q.search = a.search;
|
|
1681
|
+
if (a.limit != null) {
|
|
1682
|
+
const n = Number(a.limit);
|
|
1683
|
+
if (!Number.isInteger(n) || n < 1 || n > 200) throw new Error("limit — целое от 1 до 200.");
|
|
1684
|
+
q.limit = n;
|
|
1685
|
+
}
|
|
1686
|
+
return okResult(await api(`/api/bots/${botId}/audience/fields${qs(q)}`));
|
|
1687
|
+
}
|
|
1688
|
+
case "audience_search": {
|
|
1689
|
+
const filter = audienceFilterArg(a.filter);
|
|
1690
|
+
const body = { filter, q: a.q || undefined, sort: a.sort || undefined, dir: a.dir || undefined, page: a.page, size: a.size };
|
|
1691
|
+
if (a.botId) return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/users/search`, { method: "POST", body }));
|
|
1692
|
+
const ids = Array.isArray(a.botIds) ? a.botIds.map((id) => uuidArg(id, "botIds[]")) : undefined;
|
|
1693
|
+
return okResult(await api("/api/bots/subscribers/search", { method: "POST", body: { ...body, botIds: ids } }));
|
|
1694
|
+
}
|
|
1695
|
+
case "audience_tag_edit": {
|
|
1696
|
+
const botId = uuidArg(a.botId, "botId");
|
|
1697
|
+
const chatIds = (Array.isArray(a.chatIds) ? a.chatIds : []).map((c) => chatIdArg(c));
|
|
1698
|
+
if (!chatIds.length) throw new Error("Передай chatIds — список числовых id подписчиков (audience_search/list_bot_users).");
|
|
1699
|
+
const add = strList(a.add), remove = strList(a.remove);
|
|
1700
|
+
if (!add.length && !remove.length) throw new Error("Передай add и/или remove — список тегов.");
|
|
1701
|
+
return okResult(await api(`/api/bots/${botId}/users/tags`, { method: "POST", body: { chatIds, add, remove } }));
|
|
1702
|
+
}
|
|
1703
|
+
case "audience_variable_edit": {
|
|
1704
|
+
const botId = uuidArg(a.botId, "botId");
|
|
1705
|
+
const chatIds = (Array.isArray(a.chatIds) ? a.chatIds : []).map((c) => chatIdArg(c));
|
|
1706
|
+
if (!chatIds.length) throw new Error("Передай chatIds — список числовых id подписчиков (audience_search/list_bot_users).");
|
|
1707
|
+
const set = a.set && typeof a.set === "object" && !Array.isArray(a.set) ? a.set : undefined;
|
|
1708
|
+
const unset = strList(a.unset);
|
|
1709
|
+
if ((!set || !Object.keys(set).length) && !unset.length) throw new Error("Передай set (объект {ключ: значение}) и/или unset (список ключей).");
|
|
1710
|
+
return okResult(await api(`/api/bots/${botId}/users/variables`, { method: "POST", body: { chatIds, set, unset } }));
|
|
1711
|
+
}
|
|
1712
|
+
case "audience_export": {
|
|
1713
|
+
const format = String(a.format || "csv").toLowerCase();
|
|
1714
|
+
if (!["csv", "xlsx"].includes(format)) throw new Error("format: csv | xlsx.");
|
|
1715
|
+
if (format === "xlsx" && !a.savePath) throw new Error("xlsx — бинарный формат, передай savePath (куда сохранить файл).");
|
|
1716
|
+
const filter = audienceFilterArg(a.filter);
|
|
1717
|
+
if (a.botId) {
|
|
1718
|
+
const botId = uuidArg(a.botId, "botId");
|
|
1719
|
+
const body = { filter, q: a.q || undefined, format };
|
|
1720
|
+
if (format === "xlsx") return saveBinary(await apiBinaryPost(`/api/bots/${botId}/users/export`, body), a.savePath);
|
|
1721
|
+
return exportResult(await api(`/api/bots/${botId}/users/export`, { method: "POST", body }), a.savePath);
|
|
1722
|
+
}
|
|
1723
|
+
const ids = Array.isArray(a.botIds) ? a.botIds.map((id) => uuidArg(id, "botIds[]")) : undefined;
|
|
1724
|
+
const body = { botIds: ids, filter, q: a.q || undefined, format };
|
|
1725
|
+
if (format === "xlsx") return saveBinary(await apiBinaryPost("/api/bots/subscribers/export", body), a.savePath);
|
|
1726
|
+
return exportResult(await api("/api/bots/subscribers/export", { method: "POST", body }), a.savePath);
|
|
1727
|
+
}
|
|
1728
|
+
case "dialog_search": {
|
|
1729
|
+
const q = String(a.q || "").trim();
|
|
1730
|
+
if (!q) throw new Error("Передай q — текст поиска (имя/username/id/текст сообщения).");
|
|
1731
|
+
const query = { q };
|
|
1732
|
+
if (a.limit != null) {
|
|
1733
|
+
const n = Number(a.limit);
|
|
1734
|
+
if (!Number.isInteger(n) || n < 1 || n > 60) throw new Error("limit — целое от 1 до 60.");
|
|
1735
|
+
query.limit = n;
|
|
1736
|
+
}
|
|
1737
|
+
return okResult(await api(`/api/bots/dialogs/search${qs(query)}`));
|
|
1738
|
+
}
|
|
1739
|
+
// ---- Публикации: квота, VK ----
|
|
1740
|
+
case "publications_usage": return okResult(await api(`/api/bots/${uuidArg(a.botId, "botId")}/publications/usage`));
|
|
1741
|
+
case "vk_status": return okResult(await api("/api/bots/vk/status"));
|
|
1742
|
+
case "vk_connect": {
|
|
1743
|
+
const communityId = String(a.communityId || "").trim();
|
|
1744
|
+
const accessToken = String(a.accessToken || "").trim();
|
|
1745
|
+
if (!communityId || !accessToken) throw new Error("Передай communityId и accessToken.");
|
|
1746
|
+
return okResult(await api("/api/bots/vk/connect-token", { method: "POST", body: { communityId, accessToken } }));
|
|
1747
|
+
}
|
|
1748
|
+
case "vk_disconnect": {
|
|
1749
|
+
const accountId = idArg(a.accountId, "accountId");
|
|
1750
|
+
await api(`/api/bots/vk/${accountId}`, { method: "DELETE" });
|
|
1751
|
+
return okResult(`🗑️ VK-аккаунт ${accountId} отключён.`);
|
|
1752
|
+
}
|
|
1753
|
+
case "vk_post": {
|
|
1754
|
+
const accountId = idArg(a.accountId, "accountId");
|
|
1755
|
+
const text = a.text == null ? "" : String(a.text);
|
|
1756
|
+
const mediaUrl = a.mediaUrl ? String(a.mediaUrl) : undefined;
|
|
1757
|
+
if (!text.trim() && !mediaUrl) throw new Error("Пустой пост: передай text и/или mediaUrl (файл из upload_file, только фото).");
|
|
1758
|
+
return okResult(await api("/api/bots/vk/post", { method: "POST", body: { accountId, text, mediaUrl } }));
|
|
1759
|
+
}
|
|
1760
|
+
// ---- Миграция подписчиков ----
|
|
1761
|
+
case "migration_presets": return okResult(await api("/api/bots/migration/presets"));
|
|
1762
|
+
case "migration_preview":
|
|
1763
|
+
case "migration_commit": {
|
|
1764
|
+
const botId = uuidArg(a.botId, "botId");
|
|
1765
|
+
const platform = enumArg(a.platform, "platform", ["BOTHELP", "SALEBOT", "SENLER", "LEADTEH", "SMARTSENDER", "MANYCHAT", "GENERIC"]);
|
|
1766
|
+
const commit = params.name === "migration_commit";
|
|
1767
|
+
if (commit && !a.mapping) throw new Error("Передай mapping — сначала проверь его через migration_preview (suggestedMapping или свой).");
|
|
1768
|
+
const suffix = commit ? "commit" : "preview";
|
|
1769
|
+
return okResult(await migrationMultipart(`/api/bots/${botId}/migration/subscribers/${suffix}`, { filePath: a.path, platform, mapping: a.mapping }));
|
|
1770
|
+
}
|
|
1394
1771
|
// ---- Боты ----
|
|
1395
1772
|
case "create_bot": {
|
|
1396
1773
|
const platform = String(a.platform || "").trim().toUpperCase();
|
|
@@ -1542,8 +1919,9 @@ async function handleCall(params) {
|
|
|
1542
1919
|
if (ids.length !== 1) throw new Error("Рассылка по сценарию — ровно один botId (сценарий принадлежит боту).");
|
|
1543
1920
|
await assertBroadcastBots(ids);
|
|
1544
1921
|
if (!a.name) throw new Error("Передай name рассылки.");
|
|
1922
|
+
const audienceFilter = audienceFilterArg(a.audienceFilter) || { tagsAll: strList(a.tagsAll), tagsNone: strList(a.tagsNone) };
|
|
1545
1923
|
const b = await api(`/api/bots/${ids[0]}/broadcasts`, { method: "POST", body: {
|
|
1546
|
-
name: a.name, audienceFilter
|
|
1924
|
+
name: a.name, audienceFilter,
|
|
1547
1925
|
graphId: a.graphId, entryNodeId: a.entryNodeId || undefined, scheduledAt: toInstant(a.scheduledAt, "scheduledAt") } });
|
|
1548
1926
|
return okResult({ broadcastIds: [b?.id], status: b?.status, scheduledAt: b?.scheduledAt ?? null, broadcast: b });
|
|
1549
1927
|
}
|
|
@@ -1554,12 +1932,19 @@ async function handleCall(params) {
|
|
|
1554
1932
|
await assertBroadcastBots(ids);
|
|
1555
1933
|
const name = String(pick("name") || "").trim();
|
|
1556
1934
|
if (!name) throw new Error("Передай name рассылки (видно только тебе в списке рассылок).");
|
|
1935
|
+
const audienceFilter = audienceFilterArg(a.audienceFilter);
|
|
1936
|
+
const dedup = a.dedup === true;
|
|
1937
|
+
const stagger = staggerArg(a.stagger);
|
|
1557
1938
|
const body = {
|
|
1558
1939
|
name, botIds: ids,
|
|
1559
|
-
|
|
1560
|
-
|
|
1940
|
+
...(audienceFilter ? { audienceFilter } : {
|
|
1941
|
+
tagsAll: strList(a.tagsAll !== undefined ? a.tagsAll : draft?.audienceFilter?.tagsAll),
|
|
1942
|
+
tagsNone: strList(a.tagsNone !== undefined ? a.tagsNone : draft?.audienceFilter?.tagsNone),
|
|
1943
|
+
}),
|
|
1561
1944
|
messages: normalizeBroadcastMessages(pick("messages")),
|
|
1562
1945
|
scheduledAt: toInstant(pick("scheduledAt"), "scheduledAt"),
|
|
1946
|
+
...(dedup ? { dedup } : {}),
|
|
1947
|
+
...(stagger ? { stagger } : {}),
|
|
1563
1948
|
};
|
|
1564
1949
|
if (body.scheduledAt && new Date(body.scheduledAt).getTime() <= Date.now()) {
|
|
1565
1950
|
// Прошедшее время бэкенд молча считает «сейчас» — для черновика с устаревшей датой это сюрприз.
|
|
@@ -1569,6 +1954,8 @@ async function handleCall(params) {
|
|
|
1569
1954
|
const r = await api("/api/bots/broadcasts/direct", { method: "POST", body });
|
|
1570
1955
|
let draftDeleted = false;
|
|
1571
1956
|
if (a.draftId) { try { await api(`/api/bots/broadcasts/drafts/${a.draftId}`, { method: "DELETE" }); draftDeleted = true; } catch { /* не критично */ } }
|
|
1957
|
+
// dedup/stagger → бэкенд создаёт BroadcastGroup вместо прямых TgBroadcast (другая форма ответа: id/perBot[]/status вместо broadcastIds/totalAudience).
|
|
1958
|
+
if (dedup || stagger) return okResult({ group: r, isGroup: true, ...(a.draftId ? { draftDeleted } : {}) });
|
|
1572
1959
|
return okResult({ ...r, scheduledAt: body.scheduledAt, ...(a.draftId ? { draftDeleted } : {}) });
|
|
1573
1960
|
}
|
|
1574
1961
|
case "broadcast_cancel":
|
|
@@ -1590,9 +1977,12 @@ async function handleCall(params) {
|
|
|
1590
1977
|
if (!["DAILY", "MONTHLY", "YEARLY"].includes(recurrence)) throw new Error("recurrence: DAILY | MONTHLY | YEARLY.");
|
|
1591
1978
|
const firstRunAt = toInstant(a.firstRunAt, "firstRunAt");
|
|
1592
1979
|
if (!firstRunAt || new Date(firstRunAt).getTime() <= Date.now()) throw new Error("firstRunAt — время первого запуска в будущем (ISO 8601, без пояса — московское).");
|
|
1980
|
+
const audienceFilter = audienceFilterArg(a.audienceFilter);
|
|
1593
1981
|
return okResult(await api(base, { method: "POST", body: {
|
|
1594
|
-
name: a.name, botIds: ids,
|
|
1595
|
-
|
|
1982
|
+
name: a.name, botIds: ids,
|
|
1983
|
+
...(audienceFilter ? { audienceFilter } : { tagsAll: strList(a.tagsAll), tagsNone: strList(a.tagsNone) }),
|
|
1984
|
+
messages: normalizeBroadcastMessages(a.messages), recurrence, firstRunAt,
|
|
1985
|
+
...(a.dedup === true ? { dedup: true } : {}), ...(a.stagger != null ? { stagger: staggerArg(a.stagger) } : {}) } }));
|
|
1596
1986
|
}
|
|
1597
1987
|
throw new Error("action: list | create | stop.");
|
|
1598
1988
|
}
|
|
@@ -1624,12 +2014,189 @@ async function handleCall(params) {
|
|
|
1624
2014
|
if (a.draftId) return okResult(await api(`/api/bots/broadcasts/drafts/${a.draftId}/duplicate`, { method: "POST" }));
|
|
1625
2015
|
if (!a.broadcastId) throw new Error("Передай broadcastId или draftId.");
|
|
1626
2016
|
return okResult(await api(`/api/bots/broadcasts/${a.broadcastId}/duplicate`, { method: "POST" }));
|
|
2017
|
+
case "broadcast_group": {
|
|
2018
|
+
const base = "/api/bots/broadcasts/group";
|
|
2019
|
+
if (a.action === "list") return okResult(await api(base));
|
|
2020
|
+
if (a.action === "preview") {
|
|
2021
|
+
const ids = botIdsOf(a);
|
|
2022
|
+
if (!ids.length) throw new Error("Передай botIds (1–20 ботов).");
|
|
2023
|
+
await assertBroadcastBots(ids);
|
|
2024
|
+
const filter = audienceFilterArg(a.audienceFilter);
|
|
2025
|
+
const body = { botIds: ids, ...(filter ? { audienceFilter: filter } : { tagsAll: strList(a.tagsAll), tagsNone: strList(a.tagsNone) }) };
|
|
2026
|
+
if (a.dedup === true) body.dedup = true;
|
|
2027
|
+
const stagger = staggerArg(a.stagger);
|
|
2028
|
+
if (stagger) body.stagger = stagger;
|
|
2029
|
+
return okResult(await api(`${base}-preview`, { method: "POST", body }));
|
|
2030
|
+
}
|
|
2031
|
+
const groupId = uuidArg(a.groupId, "groupId");
|
|
2032
|
+
if (a.action === "get") return okResult(await api(`${base}/${groupId}`));
|
|
2033
|
+
if (["pause", "resume", "cancel"].includes(a.action)) {
|
|
2034
|
+
try {
|
|
2035
|
+
await api(`${base}/${groupId}/${a.action}`, { method: "POST" });
|
|
2036
|
+
} catch (e) {
|
|
2037
|
+
if (e.status === 409) throw new Error(`Группа ${groupId} сейчас не в состоянии для действия «${a.action}» (см. broadcast_group {action:"get"}).`);
|
|
2038
|
+
throw e;
|
|
2039
|
+
}
|
|
2040
|
+
const verbs = { pause: "⏸️ поставлена на паузу", resume: "▶️ возобновлена", cancel: "⏹️ отменяется" };
|
|
2041
|
+
return okResult(`Группа ${groupId} ${verbs[a.action]}.`);
|
|
2042
|
+
}
|
|
2043
|
+
throw new Error("action: preview | list | get | pause | resume | cancel.");
|
|
2044
|
+
}
|
|
1627
2045
|
case "site_templates": {
|
|
1628
2046
|
const r = await api("/api/bots/pages/templates");
|
|
1629
2047
|
let templates = Array.isArray(r?.templates) ? r.templates : [];
|
|
1630
2048
|
if (a.category) templates = templates.filter((t) => t?.category === a.category);
|
|
1631
2049
|
return okResult({ categories: r?.categories ?? [], templates });
|
|
1632
2050
|
}
|
|
2051
|
+
// ---- Публикации: цепочки ----
|
|
2052
|
+
case "chain_list": return okResult(await api("/api/bots/chains"));
|
|
2053
|
+
case "chain_get": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}`));
|
|
2054
|
+
case "chain_create": {
|
|
2055
|
+
if (!a.chain || typeof a.chain !== "object") throw new Error("Передай chain — объект PublishChain (см. описание chain_create).");
|
|
2056
|
+
return okResult(await api("/api/bots/chains", { method: "POST", body: a.chain }));
|
|
2057
|
+
}
|
|
2058
|
+
case "chain_update": {
|
|
2059
|
+
const chainId = uuidArg(a.chainId, "chainId");
|
|
2060
|
+
if (!a.chain || typeof a.chain !== "object") throw new Error("Передай chain — объект PublishChain целиком (сначала прочитай chain_get).");
|
|
2061
|
+
return okResult(await api(`/api/bots/chains/${chainId}`, { method: "PUT", body: a.chain }));
|
|
2062
|
+
}
|
|
2063
|
+
case "chain_delete": {
|
|
2064
|
+
const chainId = uuidArg(a.chainId, "chainId");
|
|
2065
|
+
if (a.confirm !== true) throw new Error("Удаление цепочки необратимо (теряется история заданий). Спроси пользователя и повтори с confirm:true.");
|
|
2066
|
+
await api(`/api/bots/chains/${chainId}`, { method: "DELETE" });
|
|
2067
|
+
return okResult(`🗑️ Цепочка ${chainId} удалена.`);
|
|
2068
|
+
}
|
|
2069
|
+
case "chain_enable": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/enable`, { method: "POST" }));
|
|
2070
|
+
case "chain_pause": return okResult(await api(`/api/bots/chains/${uuidArg(a.chainId, "chainId")}/pause`, { method: "POST" }));
|
|
2071
|
+
case "chain_runs": {
|
|
2072
|
+
const chainId = uuidArg(a.chainId, "chainId");
|
|
2073
|
+
const list = await api(`/api/bots/chains/${chainId}/runs`);
|
|
2074
|
+
// Бэкенд не умеет page/size на этом эндпоинте — режем список здесь же, в MCP (см. описание инструмента).
|
|
2075
|
+
if (!Array.isArray(list) || (a.page == null && a.size == null)) return okResult(list);
|
|
2076
|
+
const size = a.size != null ? Math.max(1, Number(a.size) || 20) : 20;
|
|
2077
|
+
const page = a.page != null ? Math.max(0, Number(a.page) || 0) : 0;
|
|
2078
|
+
const start = page * size;
|
|
2079
|
+
return okResult({ content: list.slice(start, start + size), totalElements: list.length, page, size });
|
|
2080
|
+
}
|
|
2081
|
+
case "chain_approve": return okResult(await api(`/api/bots/chains/jobs/${uuidArg(a.jobId, "jobId")}/approve`, { method: "POST" }));
|
|
2082
|
+
case "chain_reject": return okResult(await api(`/api/bots/chains/jobs/${uuidArg(a.jobId, "jobId")}/reject`, { method: "POST" }));
|
|
2083
|
+
case "chain_retry": {
|
|
2084
|
+
const jobId = uuidArg(a.jobId, "jobId");
|
|
2085
|
+
await api(`/api/bots/chains/jobs/${jobId}/retry`, { method: "POST" });
|
|
2086
|
+
return okResult(`🔁 Задание ${jobId} поставлено на повтор.`);
|
|
2087
|
+
}
|
|
2088
|
+
// ---- Аналитика (дашборд) ----
|
|
2089
|
+
case "site_analytics": {
|
|
2090
|
+
const siteId = uuidArg(a.siteId, "siteId");
|
|
2091
|
+
const qs = [];
|
|
2092
|
+
if (a.from) qs.push(`from=${encodeURIComponent(dateArg(a.from, "from"))}`);
|
|
2093
|
+
if (a.to) qs.push(`to=${encodeURIComponent(dateArg(a.to, "to"))}`);
|
|
2094
|
+
return okResult(await api(`/api/bots/pages/${siteId}/analytics${qs.length ? `?${qs.join("&")}` : ""}`));
|
|
2095
|
+
}
|
|
2096
|
+
case "site_analytics_events": {
|
|
2097
|
+
const siteId = uuidArg(a.siteId, "siteId");
|
|
2098
|
+
const qs = [];
|
|
2099
|
+
if (a.from) qs.push(`from=${encodeURIComponent(dateArg(a.from, "from"))}`);
|
|
2100
|
+
if (a.to) qs.push(`to=${encodeURIComponent(dateArg(a.to, "to"))}`);
|
|
2101
|
+
if (a.page != null) qs.push(`page=${encodeURIComponent(a.page)}`);
|
|
2102
|
+
if (a.size != null) qs.push(`size=${encodeURIComponent(a.size)}`);
|
|
2103
|
+
return okResult(await api(`/api/bots/pages/${siteId}/analytics/events${qs.length ? `?${qs.join("&")}` : ""}`));
|
|
2104
|
+
}
|
|
2105
|
+
case "bot_subscriber_analytics": {
|
|
2106
|
+
const botId = uuidArg(a.botId, "botId");
|
|
2107
|
+
const from = dateArg(a.from, "from"), to = dateArg(a.to, "to");
|
|
2108
|
+
return okResult(await api(`/api/bots/${botId}/analytics/subscribers?from=${from}&to=${to}`));
|
|
2109
|
+
}
|
|
2110
|
+
case "scenario_timeseries": {
|
|
2111
|
+
const graphId = uuidArg(a.graphId, "graphId");
|
|
2112
|
+
const from = dateArg(a.from, "from"), to = dateArg(a.to, "to");
|
|
2113
|
+
return okResult(await api(`/api/bots/graphs/${graphId}/analytics/timeseries?from=${from}&to=${to}`));
|
|
2114
|
+
}
|
|
2115
|
+
// ---- ИИ-агенты: инструменты ----
|
|
2116
|
+
case "agent_tool_test": {
|
|
2117
|
+
if (!a.agentId) throw new Error("Передай agentId.");
|
|
2118
|
+
const toolId = idArg(a.toolId, "toolId");
|
|
2119
|
+
return okResult(await api(`/api/bots/agents/${a.agentId}/tools/${toolId}/test`, { method: "POST", body: { params: a.params || {} } }));
|
|
2120
|
+
}
|
|
2121
|
+
// ---- Заявки сайта: файлы ----
|
|
2122
|
+
case "site_lead_file_download": {
|
|
2123
|
+
const siteId = uuidArg(a.siteId, "siteId");
|
|
2124
|
+
const leadId = uuidArg(a.leadId, "leadId");
|
|
2125
|
+
const fileId = uuidArg(a.fileId, "fileId");
|
|
2126
|
+
const meta = await siteLeadFileHead(siteId, leadId, fileId);
|
|
2127
|
+
return okResult({ ...meta, authHint: "Скачать: curl -H \"Authorization: Bearer <твой zmcp_-токен>\" \"<downloadUrl>\" -o файл — либо открой заявку в кабинете (сайт → «Заявки»)." });
|
|
2128
|
+
}
|
|
2129
|
+
// ---- Вебхук-сценарии ----
|
|
2130
|
+
case "scenario_list": return okResult(await api("/api/scenarios"));
|
|
2131
|
+
case "scenario_create": {
|
|
2132
|
+
const name = String(a.name ?? "").trim();
|
|
2133
|
+
if (!name) throw new Error("Передай name — название сценария.");
|
|
2134
|
+
const source = a.botId != null && String(a.botId).trim() !== ""
|
|
2135
|
+
? { type: "BOT", config: { botId: uuidArg(a.botId, "botId") } }
|
|
2136
|
+
: { type: "WEBHOOK", config: {} };
|
|
2137
|
+
return okResult(await api("/api/scenarios", { method: "POST", body: { name, source } }));
|
|
2138
|
+
}
|
|
2139
|
+
case "scenario_get": return okResult(await api(`/api/scenarios/${uuidArg(a.scenarioId, "scenarioId")}`));
|
|
2140
|
+
case "scenario_update_source": {
|
|
2141
|
+
const scenarioId = uuidArg(a.scenarioId, "scenarioId");
|
|
2142
|
+
const body = {
|
|
2143
|
+
botId: a.botId != null && String(a.botId).trim() !== "" ? uuidArg(a.botId, "botId") : null,
|
|
2144
|
+
channel: a.channel != null ? enumArg(a.channel, "channel", ["TG_BOT_API", "TG_USERBOT", "IG", "MAX"]) : null,
|
|
2145
|
+
recipientFrom: a.recipientFrom != null ? String(a.recipientFrom) : null,
|
|
2146
|
+
};
|
|
2147
|
+
try {
|
|
2148
|
+
return okResult(await api(`/api/scenarios/${scenarioId}/source`, { method: "PUT", body }));
|
|
2149
|
+
} catch (e) {
|
|
2150
|
+
if (e.status === 409) throw new Error("Это не вебхук-сценарий (source.type BOT) — у бот-сценария выход не перепривязывается, это сам бот графа.");
|
|
2151
|
+
throw e;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
case "scenario_publish": {
|
|
2155
|
+
try {
|
|
2156
|
+
return okResult(await api(`/api/scenarios/${uuidArg(a.scenarioId, "scenarioId")}/publish`, { method: "POST" }));
|
|
2157
|
+
} catch (e) {
|
|
2158
|
+
if (e.status === 409) throw new Error("Это не вебхук-сценарий (source.type BOT) — публикуй через publish_graph.");
|
|
2159
|
+
throw e;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
case "scenario_rotate_secret": return okResult(await api(`/api/scenarios/${uuidArg(a.scenarioId, "scenarioId")}/rotate-secret`, { method: "POST" }));
|
|
2163
|
+
// ---- Публичный API ИИ-агента: ключи ----
|
|
2164
|
+
case "agent_api_key_list": return okResult(await api(`/api/bots/agents/${idArg(a.agentId, "agentId")}/api-keys`));
|
|
2165
|
+
case "agent_api_key_create": {
|
|
2166
|
+
const body = {};
|
|
2167
|
+
if (a.name != null && String(a.name).trim() !== "") body.name = String(a.name).trim();
|
|
2168
|
+
try {
|
|
2169
|
+
return okResult(await api(`/api/bots/agents/${idArg(a.agentId, "agentId")}/api-keys`, { method: "POST", body }));
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
if (e.status === 409) throw new Error("До 20 активных ключей на агента — сначала отзови лишний (agent_api_key_revoke).");
|
|
2172
|
+
throw e;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
case "agent_api_key_revoke": {
|
|
2176
|
+
const agentId = idArg(a.agentId, "agentId");
|
|
2177
|
+
const keyId = idArg(a.keyId, "keyId");
|
|
2178
|
+
if (a.confirm !== true) throw new Error("Отзыв ключа необратим: публичный чат с ним сразу перестанет отвечать. Спроси пользователя и повтори с confirm:true.");
|
|
2179
|
+
await api(`/api/bots/agents/${agentId}/api-keys/${keyId}`, { method: "DELETE" });
|
|
2180
|
+
return okResult(`🗑️ Ключ ${keyId} отозван.`);
|
|
2181
|
+
}
|
|
2182
|
+
// ---- Пакеты ответов ИИ ----
|
|
2183
|
+
case "ai_packs": {
|
|
2184
|
+
const [skus, sub] = await Promise.all([
|
|
2185
|
+
api("/api/bots/ai-packs"),
|
|
2186
|
+
api("/api/bots/subscription").catch(() => null),
|
|
2187
|
+
]);
|
|
2188
|
+
return okResult({
|
|
2189
|
+
skus,
|
|
2190
|
+
balance: sub ? { aiPackAnswersLeft: sub.aiPackAnswersLeft, aiPackTokensLeft: sub.aiPackTokensLeft } : null,
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
case "ai_pack_purchase": {
|
|
2194
|
+
const sku = String(a.sku ?? "").trim();
|
|
2195
|
+
if (!sku) throw new Error("Передай sku — из ai_packs.");
|
|
2196
|
+
if (a.confirm !== true) throw new Error("Это платёж. Спроси пользователя и повтори с confirm:true.");
|
|
2197
|
+
const r = await api("/api/bots/ai-packs/purchase", { method: "POST", body: { sku, method: a.method || "card" } });
|
|
2198
|
+
return okResult({ ...r, hint: "Пакет зачислится после подтверждения оплаты по invoiceUrl — ничего не списано автоматически." });
|
|
2199
|
+
}
|
|
1633
2200
|
default:
|
|
1634
2201
|
throw new Error(`Неизвестный инструмент: ${params && params.name}`);
|
|
1635
2202
|
}
|