@retensy/mcp 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retensy/mcp",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "MCP server to build and publish Telegram and MAX bot funnels/automations, broadcasts, service integrations, block-based websites (incl. free-layout Zero blocks, designs, templates, custom domains, form leads) and Markdown blog articles in the Retensy Bots service. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -161,6 +161,16 @@ IG-боты не поддерживают команды (`/start`). Вход
161
161
  (`SWITCH_NO_EXPRESSION`), нет случаев (`SWITCH_NO_CASES`), пустое `value` (`SWITCH_EMPTY_VALUE`),
162
162
  повтор значения (`SWITCH_DUPLICATE_VALUE`), случай без ребра (`SWITCH_CASE_UNCONNECTED`).
163
163
  Ребро на `default` необязательно.
164
+ - `AI_CLASSIFY` — **ИИ-разбор свободного текста без диалога**: понять, чего хочет клиент, ответить «да/нет» или достать
165
+ значение (телефон, бюджет) в переменную. Не путать с `AI_REPLY` (ответ клиенту). Расходует ИИ-токены тарифа (вид расхода CLASSIFY).
166
+ `{ "mode":"classify"|"yesno"|"extract", "instruction":"Определи намерение клиента", "input":"{{last_text}}" (по умолчанию),
167
+ "categories":[{"id":"buy","label":"Хочет купить"},{"id":"ask","label":"Вопрос"}] (только classify, 2-10),
168
+ "field":"номер телефона" (только extract), "saveTo":"intent" }`. Выходы: classify - `cat_<id>` + `other`;
169
+ yesno - `yes`, `no`, `other`; extract - `found`, `other`; везде ещё `error` (сбой модели). В `saveTo` ложится
170
+ id категории, `yes`/`no`/`other` или найденное значение. Кончились ИИ-токены или текст пустой - выход `other`.
171
+ Валидатор: `AI_CLASSIFY_BAD_MODE`, `AI_CLASSIFY_NO_INSTRUCTION`, `AI_CLASSIFY_FEW_CATEGORIES`, `AI_CLASSIFY_BAD_CATEGORIES`,
172
+ `AI_CLASSIFY_CATEGORY_UNCONNECTED` (категория без ребра), `AI_CLASSIFY_NO_FIELD`, `AI_CLASSIFY_NO_SAVE_TO`,
173
+ `AI_CLASSIFY_BAD_SAVE_TO`, `AI_CLASSIFY_LONG_INSTRUCTION`. Не для Instagram.
164
174
  - `STOP_AND_ERROR` — `{ "message":"CRM не ответила: {{var.http_status}}" }`. Обрывает прогон и
165
175
  помечает его в журнале как ошибочный (`FAILED`), шаг — `ok:false` с этим текстом. Выходов нет,
166
176
  ничего подписчику не отправляет. Пустой `message` → «Сценарий остановлен с ошибкой».
@@ -448,7 +458,7 @@ IG-боты не поддерживают команды (`/start`). Вход
448
458
 
449
459
  | `kind` | `op` (допустимые) | Поля | Что проверяет |
450
460
  |---|---|---|---|
451
- | `TAG` | `HAS`, `NOT_HAS` | `value` — метка `[a-z0-9_-]{1,64}` | есть ли у пользователя тег |
461
+ | `TAG` | `HAS`, `NOT_HAS`, `OLDER_THAN`, `WITHIN` | `value` — метка `[a-z0-9_-]{1,64}`; для `OLDER_THAN`/`WITHIN` ещё `tagDays` 1..3650 | есть ли тег; `OLDER_THAN` — метка стоит не меньше `tagDays` дней, `WITHIN` — выдана за последние `tagDays` дней (у меток без записанной даты выдачи — «нет»; `CONDITION_BAD_TAG_DAYS`) |
452
462
  | `VARIABLE` | `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY`, `GT`, `LT` | `key` — имя переменной `[a-z_][a-z0-9_]{0,63}`, `value` | значение переменной (`GT`/`LT` — числовое сравнение) |
453
463
  | `UTM` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `key` ∈ `source`/`medium`/`campaign`/`content`/`term`, `value` | UTM-метку клика (`utm_<key>`), регистронезависимо |
454
464
  | `NAME` | `EQUALS`, `CONTAINS`, `NOT_EMPTY`, `EMPTY` | `value` | имя из профиля, регистронезависимо |
@@ -541,6 +551,7 @@ node validate.mjs graph.json --platform=INSTAGRAM
541
551
  | `ACCESS_GATE` | `allowed`, `denied` |
542
552
  | `ACTIONS` | `next`, плюс `timeout` (необязательный: `external_request` не уложился в таймаут) и `error` — если внутри есть действие, которое может упасть (внешний запрос, CRM, Таблицы); `taken` — слот `booking_book` занят |
543
553
  | `SWITCH` | `case_<id>`, `default` |
554
+ | `AI_CLASSIFY` | classify: `cat_<id>`, `other`, `error`; yesno: `yes`, `no`, `other`, `error`; extract: `found`, `other`, `error` |
544
555
  | `RANDOM_TEXT` | `next`, `error` |
545
556
  | `STOP_AND_ERROR` | выходов нет (терминатор) |
546
557
  | `DELETE_MESSAGE` | `next` (всегда, даже если удалить не вышло) |
@@ -17,6 +17,11 @@
17
17
  - Кнопок недостаточно: сообщение только с `buttons` — пустое.
18
18
  - Пробелы/таб/перенос строки = пусто (`.isBlank()`).
19
19
 
20
+ ### `AI_CLASSIFY` — «ИИ-разбор»
21
+ - `mode` ∈ `classify|yesno|extract` (`AI_CLASSIFY_BAD_MODE`); `instruction` обязательна и ≤ 1000 знаков для classify/yesno.
22
+ - classify: 2-10 категорий с уникальными непустыми `id` (`AI_CLASSIFY_FEW_CATEGORIES`, `AI_CLASSIFY_BAD_CATEGORIES`), у каждой есть ребро `cat_<id>` (`AI_CLASSIFY_CATEGORY_UNCONNECTED`, проверяет бэкенд).
23
+ - extract: нужны `field` и `saveTo` (`AI_CLASSIFY_NO_FIELD`, `AI_CLASSIFY_NO_SAVE_TO`). `saveTo` ∈ `[a-z_][a-z0-9_]{0,63}`.
24
+
20
25
  ### `RANDOM_TEXT` — «Случайный текст»
21
26
  - `config.texts[]` — хотя бы один непустой текст (`RANDOM_TEXT_NO_TEXTS`); не больше 500 (`RANDOM_TEXT_TOO_MANY`),
22
27
  каждый ≤ 65 536 знаков (`RANDOM_TEXT_TOO_LONG`), вместе ≤ 1 500 000 (`RANDOM_TEXT_TOTAL_TOO_LONG`) - иначе сценарий не влезет в 4 МБ.
@@ -325,6 +325,30 @@ for (const n of nodes) {
325
325
  }
326
326
  break;
327
327
  }
328
+ case "AI_CLASSIFY": {
329
+ // Зеркало GraphValidator.validateAiClassify(): ИИ-разбор текста (classify / yesno / extract).
330
+ const mode = c.mode;
331
+ if (!["classify", "yesno", "extract"].includes(mode)) {
332
+ errors.push(`AI_CLASSIFY_BAD_MODE: ${who} — mode ∈ {classify,yesno,extract}.`);
333
+ break;
334
+ }
335
+ if (typeof c.instruction === "string" && c.instruction.length > 1000) errors.push(`AI_CLASSIFY_LONG_INSTRUCTION: ${who} — instruction длиннее 1000 знаков.`);
336
+ if (!blank(c.saveTo) && !VAR_RE.test(String(c.saveTo))) errors.push(`AI_CLASSIFY_BAD_SAVE_TO: ${who} — saveTo ∈ [a-z_][a-z0-9_]{0,63}.`);
337
+ if (mode === "extract") {
338
+ if (blank(c.field)) errors.push(`AI_CLASSIFY_NO_FIELD: ${who} — для extract нужно field (что достать).`);
339
+ if (blank(c.saveTo)) errors.push(`AI_CLASSIFY_NO_SAVE_TO: ${who} — для extract нужен saveTo, иначе значение теряется.`);
340
+ break;
341
+ }
342
+ if (blank(c.instruction)) errors.push(`AI_CLASSIFY_NO_INSTRUCTION: ${who} — нужна instruction.`);
343
+ if (mode === "classify") {
344
+ const cats = Array.isArray(c.categories) ? c.categories : [];
345
+ const ids = cats.map((x) => String((x && x.id) ?? "").trim());
346
+ const uniq = new Set(ids.filter(Boolean));
347
+ if (uniq.size < 2) errors.push(`AI_CLASSIFY_FEW_CATEGORIES: ${who} — нужно минимум 2 категории с уникальными id.`);
348
+ else if (cats.length > uniq.size || cats.length > 10) errors.push(`AI_CLASSIFY_BAD_CATEGORIES: ${who} — id категорий уникальные и непустые, не больше 10.`);
349
+ }
350
+ break;
351
+ }
328
352
  case "DELETE_MESSAGE": {
329
353
  // Зеркало GraphValidator.validateDeleteMessage (Java).
330
354
  if (!["NODE", "PREVIOUS", "USER_LAST", "NEXT"].includes(c.mode)) {
@@ -384,6 +408,8 @@ for (const n of nodes) {
384
408
  const kind = cond.kind;
385
409
  // Бэкенд блокирует публикацию только для TAG.value и VARIABLE.key:
386
410
  if (kind === "TAG" && !TAG_RE.test(String(cond.value || ""))) errors.push(`CONDITION_BAD_TAG: ${who} — TAG.value ∈ [a-z0-9_-]{1,64}.`);
411
+ if (kind === "TAG" && (cond.op === "OLDER_THAN" || cond.op === "WITHIN")
412
+ && !(Number.isFinite(cond.tagDays) && cond.tagDays >= 1 && cond.tagDays <= 3650)) errors.push(`CONDITION_BAD_TAG_DAYS: ${who} — TAG с OLDER_THAN/WITHIN требует tagDays 1..3650.`);
387
413
  if (kind === "VARIABLE" && !VAR_RE.test(String(cond.key || ""))) errors.push(`CONDITION_BAD_KEY: ${who} — VARIABLE.key ∈ [a-z_][a-z0-9_]{0,63}.`);
388
414
  // Остальное — подсказки (рантайм бэкенда отдаст false, но публикацию не сорвёт):
389
415
  if (!COND_OPS[kind]) warns.push(`${who}: неизвестный kind «${kind}» в условии #${i + 1} — рантайм даст false.`);
package/src/index.mjs CHANGED
@@ -1019,6 +1019,9 @@ const AUDIENCE_FILTER_SCHEMA = { type: "object", description: "Полный фи
1019
1019
  lastActivityFrom: { type: "string", description: "ISO — последняя активность от" },
1020
1020
  lastActivityTo: { type: "string" },
1021
1021
  blocked: { type: ["boolean", "null"], description: "true — заблокировал бота, false — активен, null/не задано — любой" },
1022
+ tagAges: { type: "array", description: "Давность метки (И): «метка стоит не меньше N дней» или «получена за последние N дней». У меток, выданных до появления даты выдачи, даты нет — такие подписчики не попадут.", items: { type: "object", properties: {
1023
+ tag: { type: "string" }, olderThanDays: { type: "number", description: "метка стоит не меньше N дней (1..3650)" }, withinDays: { type: "number", description: "метка выдана за последние N дней (1..3650)" },
1024
+ }, required: ["tag"] } },
1022
1025
  variableConditions: { type: "array", items: { type: "object", properties: {
1023
1026
  key: { type: "string" }, type: { type: "string", enum: ["TEXT", "NUMBER", "DATE", "BOOL"] },
1024
1027
  op: { type: "string", enum: ["EQ", "NEQ", "CONTAINS", "GT", "LT", "BETWEEN", "BEFORE", "AFTER", "IS", "EMPTY", "NOT_EMPTY"] },
@@ -1100,7 +1103,7 @@ const TOOLS = [
1100
1103
  { name: "agent_list", description: "Список ИИ-агентов пользователя (GET /api/bots/agents): карточки — id, имя, статус. Read-only.", inputSchema: { type: "object", properties: {} } },
1101
1104
  { name: "agent_get", description: "ИИ-агент по id (GET /api/bots/agents/{agentId}): настройки (имя, язык, тон, длина/формат ответа, инструкции, запретные/передаточные темы, kbId базы знаний, статус). Чужой агент — 404. Read-only.", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
1102
1105
  { name: "agent_create", description: "Создать ИИ-агента (POST /api/bots/agents): name, description — необязательны. Вместе с агентом создаётся его база знаний (kbId в ответе) — дальше kb_add_qa/kb_add_text/kb_add_site.", inputSchema: { type: "object", properties: { name: { type: "string" }, description: { type: "string" } } } },
1103
- { name: "agent_update", description: "Изменить настройки агента (PATCH /api/bots/agents/{agentId}): patch — объект с полями для правки (name, description, language: RU|EN|AUTO, tone: FRIENDLY|NEUTRAL|FORMAL, answerLength: SHORT|MEDIUM|LONG, format: PLAIN|LIST_FRIENDLY, instructions, forbiddenTopics[], handoffTopics[], fallback, tools[]). Применяется частично — передавай только то, что меняешь. tools[] заменяет список целиком (до 10). Инструменты ЧТЕНИЯ: AMO_LOOKUP {amoConnectionId}, SHEETS_LOOKUP {sheetsEmail,sheetsSpreadsheetId,sheetsLookupColumn,…}, HTTP_API {httpUrlTemplate,httpParams}. Инструменты ЗАПИСИ (включаются явно по одному, id a-z0-9_): AMO_CREATE_LEAD {amoConnectionId, amoLeadName, amoContactName?, amoPhone?, amoEmail?, amoPrice?, amoPipelineId?, amoStatusId?}, AMO_UPDATE_LEAD {amoLeadId + что обновлять}, AMO_UPDATE_CONTACT {amoContactId, amoContactName?|amoPhone?|amoEmail?}, SHEETS_APPEND {sheetsEmail,sheetsSpreadsheetId,sheetsSheetName?, sheetsRowTemplate:[ячейки]}, HTTP_POST {httpUrlTemplate, httpBodyTemplate? (JSON-шаблон), httpHeaders?:[{name,value}], httpJsonPath?}, SET_FIELD {allowedFields[]}, ADD_TAG {allowedTags[]}, HANDOFF. Для записи (кроме SET_FIELD/ADD_TAG/HANDOFF) обязательна argsSchema — плоская JSON-схема {properties:{имя:{type:string|number|integer|boolean, description?, enum?, pattern?, maxLength?, minLength?, minimum?, maximum?}}, required?:[]}; в шаблонах {имя} подставляет аргумент, {now} — время. Общие поля записи: confirmBeforeRun (по умолчанию true для внешних записей — агент сначала спрашивает согласие пользователя), limitPerConversation (по умолч. 3, макс 20), limitPerDay (по умолч. 100, макс 1000). В публичном веб-чате инструменты записи не работают. Перед отправкой tools[] проверяются локально, ошибки схем возвращаются списком.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, patch: { type: "object", description: "Поля агента для частичного обновления" } }, required: ["agentId", "patch"] } },
1106
+ { name: "agent_update", description: "Изменить настройки агента (PATCH /api/bots/agents/{agentId}): patch — объект с полями для правки (name, description, language: RU|EN|AUTO, tone: FRIENDLY|NEUTRAL|FORMAL, answerLength: SHORT|MEDIUM|LONG, format: PLAIN|LIST_FRIENDLY, instructions, forbiddenTopics[], handoffTopics[], fallback, citeSources: boolean, guardrail: boolean, tools[]). guardrail:true — перед отправкой ответ проверяется: цена, процент или обещание (гарантия, возврат, бесплатно), которых нет в базе знаний и данных инструментов, клиенту не уходят — ход идёт по «не знаю» (фолбэк агента, в том числе передача оператору); причина видна в agent_unanswered/логе хода. Применяется частично — передавай только то, что меняешь. tools[] заменяет список целиком (до 10). Инструменты ЧТЕНИЯ: AMO_LOOKUP {amoConnectionId}, SHEETS_LOOKUP {sheetsEmail,sheetsSpreadsheetId,sheetsLookupColumn,…}, HTTP_API {httpUrlTemplate,httpParams}. Инструменты ЗАПИСИ (включаются явно по одному, id a-z0-9_): AMO_CREATE_LEAD {amoConnectionId, amoLeadName, amoContactName?, amoPhone?, amoEmail?, amoPrice?, amoPipelineId?, amoStatusId?}, AMO_UPDATE_LEAD {amoLeadId + что обновлять}, AMO_UPDATE_CONTACT {amoContactId, amoContactName?|amoPhone?|amoEmail?}, SHEETS_APPEND {sheetsEmail,sheetsSpreadsheetId,sheetsSheetName?, sheetsRowTemplate:[ячейки]}, HTTP_POST {httpUrlTemplate, httpBodyTemplate? (JSON-шаблон), httpHeaders?:[{name,value}], httpJsonPath?}, SET_FIELD {allowedFields[]}, ADD_TAG {allowedTags[]}, HANDOFF. Для записи (кроме SET_FIELD/ADD_TAG/HANDOFF) обязательна argsSchema — плоская JSON-схема {properties:{имя:{type:string|number|integer|boolean, description?, enum?, pattern?, maxLength?, minLength?, minimum?, maximum?}}, required?:[]}; в шаблонах {имя} подставляет аргумент, {now} — время. Общие поля записи: confirmBeforeRun (по умолчанию true для внешних записей — агент сначала спрашивает согласие пользователя), limitPerConversation (по умолч. 3, макс 20), limitPerDay (по умолч. 100, макс 1000). В публичном веб-чате инструменты записи не работают. Перед отправкой tools[] проверяются локально, ошибки схем возвращаются списком.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, patch: { type: "object", description: "Поля агента для частичного обновления" } }, required: ["agentId", "patch"] } },
1104
1107
  { name: "agent_publish", description: "Опубликовать агента (POST /api/bots/agents/{agentId}/publish): агент начинает отвечать в подключённых сценариях. HTTP 409 CHECKLIST_FAILED с чеклистом, если агент ещё не готов (нет базы знаний, пустые инструкции и т.п.) — агент не меняется.", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
1105
1108
  { name: "agent_health", description: "Здоровье базы знаний агента (GET /api/bots/agents/{agentId}/health): счётчики документов/фрагментов по запросу, без кэша. Read-only.", inputSchema: { type: "object", properties: { agentId: { type: "string" } }, required: ["agentId"] } },
1106
1109
  { name: "agent_test_chat", description: "Проверить ответ агента в песочнице без отправки клиенту (POST /api/bots/agents/{agentId}/test-chat): question (1–1000 символов), history — необязательная история диалога [{role: client|agent, text}], до 12 реплик. ТРАТИТ бюджет ИИ, как настоящий ответ — не вызывай массово.", inputSchema: { type: "object", properties: { agentId: { type: "string" }, question: { type: "string" }, history: { type: "array", items: { type: "object", properties: { role: { type: "string", enum: ["client", "agent"] }, text: { type: "string" } } } } }, required: ["agentId", "question"] } },
@@ -1165,7 +1168,7 @@ const TOOLS = [
1165
1168
  { name: "kb_delete_doc", description: "Удалить документ из базы знаний вместе с его фрагментами (DELETE /api/bots/kb/{kbId}/docs/{docId}). Необратимо.", inputSchema: { type: "object", properties: { kbId: { type: "string" }, docId: { type: "string" } }, required: ["kbId", "docId"] } },
1166
1169
  { name: "bot_user_get", description: "Карточка подписчика (GET /api/bots/{botId}/users/{chatId}): теги, переменные (поля, ответы, ai_summary), блокировка, счётчики. chatId — из list_bot_users. Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string", description: "числовой chatId (строкой — без потери точности)" } }, required: ["botId", "chatId"] } },
1167
1170
  { name: "bot_user_runs", description: "Журнал запусков сценариев подписчика (GET /api/bots/{botId}/users/{chatId}/runs): триггер, статус, шаги с результатом (ветка ИИ-агента, ok/ошибка каждого действия — CRM, уведомление, HTTP). Так проверяют, что сценарий реально сделал нужное. Статус прогона: OK, FAILED (прерван) или PARTIAL — «завершён с ошибками»: дошёл до конца, но хотя бы одно действие упало (у шага ok:false, error «КОД: …»). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["botId", "chatId"] } },
1168
- { name: "dialog_messages", description: "Переписка с подписчиком, свежие сверху (GET /api/bots/{botId}/users/{chatId}/messages, page/size). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["botId", "chatId"] } },
1171
+ { name: "dialog_messages", description: "Переписка с подписчиком, свежие сверху (GET /api/bots/{botId}/users/{chatId}/messages, page/size). Поле sender: null — бот, OPERATOR — человек, AI — ответ модели ИИ-агента, HANDOFF — передача оператору; corrected:true — ответ ИИ исправлен оператором (правильный ответ уже в базе знаний). Read-only.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, page: { type: "number" }, size: { type: "number" } }, required: ["botId", "chatId"] } },
1169
1172
  { name: "dialog_reply", description: "ОТПРАВИТЬ сообщение подписчику от имени оператора (POST /api/bots/{botId}/users/{chatId}/messages) — как ответ из раздела «Диалоги». Уходит РЕАЛЬНОМУ человеку: только по явной просьбе пользователя. Бот при этом не останавливается.", inputSchema: { type: "object", properties: { botId: { type: "string" }, chatId: { type: "string" }, text: { type: "string" } }, required: ["botId", "chatId", "text"] } },
1170
1173
  // ---- Паритет платформы: журнал вызовов, оператор, заявки, виджет, подписчики, бот ----
1171
1174
  { name: "integration_calls", description: "Журнал вызовов внешних сервисов из сценариев (GET /api/bots/integrations/calls): время, подключение, действие, ok/ошибка, код, попытки, correlationId (= runId прогона). Фильтры: connectionId (из list_integrations), ok (true — только успешные, false — только упавшие), limit (1–200). Так проверяют, что CRM/таблица/HTTP реально получили данные. Read-only.", inputSchema: { type: "object", properties: { connectionId: { type: "string" }, ok: { type: "boolean" }, limit: { type: "number" } } } },
@@ -1268,8 +1271,8 @@ const TOOLS = [
1268
1271
  { 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"] } },
1269
1272
  { 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"] } },
1270
1273
  // ---- Пакеты ответов ИИ сверх тарифа (решение владельца 14.10) ----
1271
- { 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: {} } },
1272
- { 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"] } },
1274
+ { name: "ai_packs", description: "Пакеты ответов ИИ сверх месячного бюджета тарифа: каталог (GET /api/bots/ai-packs): offers — разовые пакеты [{sku, priceRub, tokens, approxAnswers}], periodOffers — те же на 1/3/12 мес. (months, priceRub — итог за период со скидкой, savingsRub — экономия, tokens/approxAnswers — за МЕСЯЦ, начисляются помесячно) (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: {} } },
1275
+ { 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" }, period: { type: "integer", enum: [1, 3, 12], description: "период, мес.: 1 (по умолчанию), 3 (скидка 10%) или 12 (скидка 20%) — токены начисляются каждый месяц, не сразу; можно вместо этого передать sku периода из periodOffers (ai_pack_5000_m3)" }, method: { type: "string", description: "card (по умолчанию) и т.п., как у subscribe" }, confirm: { type: "boolean" } }, required: ["sku", "confirm"] } },
1273
1276
  ];
1274
1277
 
1275
1278
  async function handleCall(params) {
@@ -2701,7 +2704,9 @@ async function handleCall(params) {
2701
2704
  const sku = String(a.sku ?? "").trim();
2702
2705
  if (!sku) throw new Error("Передай sku — из ai_packs.");
2703
2706
  if (a.confirm !== true) throw new Error("Это платёж. Спроси пользователя и повтори с confirm:true.");
2704
- const r = await api("/api/bots/ai-packs/purchase", { method: "POST", body: { sku, method: a.method || "card" } });
2707
+ const period = a.period === undefined ? undefined : Number(a.period);
2708
+ if (period !== undefined && ![1, 3, 12].includes(period)) throw new Error("period: 1, 3 или 12 месяцев.");
2709
+ const r = await api("/api/bots/ai-packs/purchase", { method: "POST", body: { sku, method: a.method || "card", ...(period ? { period } : {}) } });
2705
2710
  return okResult({ ...r, hint: "Пакет зачислится после подтверждения оплаты по invoiceUrl — ничего не списано автоматически." });
2706
2711
  }
2707
2712
  default: