@besales/mcp 1.0.0-beta.2 → 1.0.0-beta.5

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.
Files changed (73) hide show
  1. package/README.md +14 -3
  2. package/dist/http/api-client.d.ts +10 -0
  3. package/dist/http/api-client.js +2 -1
  4. package/dist/http/api-client.js.map +1 -1
  5. package/dist/resources/concepts/triggers.md +35 -0
  6. package/dist/resources/docs/platforms.md +58 -0
  7. package/dist/schemas/mcp-tools.json +892 -40
  8. package/dist/tools/definitions/agent-design.d.ts +5 -5
  9. package/dist/tools/definitions/ai-costs.d.ts +1 -1
  10. package/dist/tools/definitions/api-tool-request.d.ts +4 -2
  11. package/dist/tools/definitions/broadcast-analytics.d.ts +1 -1
  12. package/dist/tools/definitions/crm-platform.d.ts +5 -2
  13. package/dist/tools/definitions/crm-platform.js +30 -0
  14. package/dist/tools/definitions/crm-platform.js.map +1 -1
  15. package/dist/tools/definitions/dialogue-export.d.ts +1 -1
  16. package/dist/tools/definitions/dialogue-nudge.d.ts +7 -0
  17. package/dist/tools/definitions/dialogue-nudge.js +139 -0
  18. package/dist/tools/definitions/dialogue-nudge.js.map +1 -0
  19. package/dist/tools/definitions/dialogue-reprocess.d.ts +5 -0
  20. package/dist/tools/definitions/dialogue-reprocess.js +107 -0
  21. package/dist/tools/definitions/dialogue-reprocess.js.map +1 -0
  22. package/dist/tools/definitions/follow-up.d.ts +1 -1
  23. package/dist/tools/definitions/funnel-analytics.d.ts +1 -1
  24. package/dist/tools/definitions/getcourse-cold-welcome.js +18 -7
  25. package/dist/tools/definitions/getcourse-cold-welcome.js.map +1 -1
  26. package/dist/tools/definitions/knowledge.d.ts +1 -1
  27. package/dist/tools/definitions/model-comparison.d.ts +1 -1
  28. package/dist/tools/definitions/platform-setup-wave.d.ts +1 -1
  29. package/dist/tools/definitions/platform-setup.d.ts +12 -1
  30. package/dist/tools/definitions/shared.d.ts +5 -0
  31. package/dist/tools/definitions/shared.js.map +1 -1
  32. package/dist/tools/definitions/task-state.d.ts +4 -4
  33. package/dist/tools/definitions/telegram-personal-management.d.ts +34 -0
  34. package/dist/tools/definitions/telegram-personal-management.js +139 -0
  35. package/dist/tools/definitions/telegram-personal-management.js.map +1 -0
  36. package/dist/tools/definitions/telegram-personal-profile.d.ts +24 -0
  37. package/dist/tools/definitions/telegram-personal-profile.js +154 -0
  38. package/dist/tools/definitions/telegram-personal-profile.js.map +1 -0
  39. package/dist/tools/definitions/telegram-personal-restrictions.d.ts +19 -0
  40. package/dist/tools/definitions/telegram-personal-restrictions.js +109 -0
  41. package/dist/tools/definitions/telegram-personal-restrictions.js.map +1 -0
  42. package/dist/tools/definitions/telegram-pool-policy.d.ts +1 -1
  43. package/dist/tools/definitions/trigger-upsert.tool.d.ts +12 -0
  44. package/dist/tools/definitions/trigger-upsert.tool.js +38 -1
  45. package/dist/tools/definitions/trigger-upsert.tool.js.map +1 -1
  46. package/dist/tools/definitions.d.ts +117 -25
  47. package/dist/tools/definitions.js +10 -0
  48. package/dist/tools/definitions.js.map +1 -1
  49. package/dist/tools/follow-up-request.js +3 -0
  50. package/dist/tools/follow-up-request.js.map +1 -1
  51. package/dist/tools/registry-mapping-primary.cases.js +12 -0
  52. package/dist/tools/registry-mapping-primary.cases.js.map +1 -1
  53. package/dist/tools/registry-mapping.cases.js +230 -0
  54. package/dist/tools/registry-mapping.cases.js.map +1 -1
  55. package/dist/tools/registry-test-fixtures.js +5 -1
  56. package/dist/tools/registry-test-fixtures.js.map +1 -1
  57. package/dist/tools/registry.js +3 -1
  58. package/dist/tools/registry.js.map +1 -1
  59. package/dist/types/api-contract.gen.d.ts +81138 -55369
  60. package/docs/dialogue-nudge.md +50 -0
  61. package/docs/dialogue-reprocess.md +37 -0
  62. package/docs/modules/besales-mcp.md +7 -0
  63. package/docs/telegram-personal-management.md +100 -0
  64. package/docs/telegram-personal-restrictions.md +87 -0
  65. package/package.json +1 -1
  66. package/scripts/getcourse-text-source.spec.mjs +110 -0
  67. package/scripts/shipped-sample.mjs +3 -0
  68. package/scripts/sync-trigger-argument-schema.mjs +10 -34
  69. package/scripts/sync-trigger-argument-schema.spec.mjs +91 -0
  70. package/scripts/sync-trigger-event-schema.mjs +20 -0
  71. package/scripts/sync-trigger-task-schema.mjs +45 -0
  72. package/scripts/telegram-personal-confirmation-mutants.mjs +74 -0
  73. package/scripts/telegram-personal-profile-mutants.mjs +96 -0
@@ -0,0 +1,50 @@
1
+ # Подтолкнуть агента
2
+
3
+ `besales_dialogue_nudge_agent` нужен, когда клиент остался без ответа из-за ошибки или
4
+ сбоя бота, а переобработка последнего входящего не подходит. Он запускает полный ход
5
+ по всей истории диалога. Это платная операция: клиент получит настоящее сообщение бота.
6
+ Инструмент отказывает (`REFUSED`, при тишине обычно `BOT_SILENCED`) при тишине,
7
+ поставленной человеком/оператором (ручная остановка, стоп-слово), отписке, блокировке,
8
+ бане, вне срока переписки канала, при выключенном канале/ИИ и пока ход уже идёт.
9
+ Отказ сервера не обходи.
10
+
11
+ Служебную тишину (ожидание оператора «до /start», стадия CRM, фильтр сделки,
12
+ тишина агента) инструмент снимает и возвращает `reactivated: true`, как кнопка в интерфейсе.
13
+ Перед подтверждением предупреди человека: клиент получит настоящее сообщение бота
14
+ даже при служебном ожидании оператора. Покажи `reactivated` в итоге.
15
+
16
+ Только после явного «да» человека передай `explicit_confirmation: true` и
17
+ `conversation_ids`: от 1 до 30 UUID без дублей. `workspace_id` необязателен; без него
18
+ используется выбранное подключение. Агент выбирается сервером: тело запроса всегда `{}`.
19
+ API должен разрешать MCP-ключу область `run:conversation-nudge`.
20
+
21
+ Для каждого диалога мост отправляет `POST /conversations/{id}/nudge-agent` с заголовком
22
+ `Idempotency-Key: nudge:<UUID в нижнем регистре>:<SHA-256 attempt_key>`.
23
+ Необязательный `attempt_key` — строка до 64 символов, по умолчанию пустая. Тот же вход
24
+ отправляет тот же ключ, независимо от порядка диалогов в пакете. Сервер возвращает
25
+ прежний результат без второго ответа клиенту. Для нового осознанного запуска позже
26
+ можно задать новый `attempt_key`, только после нового явного подтверждения.
27
+
28
+ Запросы последовательные. Отчёт разделяет:
29
+
30
+ - `STARTED` — ход принят. `delivery` отдельно показывает `DELIVERED`, `NOT_DELIVERED`
31
+ или `PENDING`; `PENDING` не подтверждает доставку. Сохраняются `notice`, `reasonCode`,
32
+ `reactivated` и `replayed`. При `replayed=true` нового запуска не было.
33
+ Если беседа была закрыта (`CLOSED`/`RESOLVED`) и текущей нет, сервер создаёт новую:
34
+ `conversationId` в отчёте сохраняет запрошенный UUID, `startedConversationId` содержит
35
+ новый UUID, а `message` поясняет «диалог был закрыт — создан новый».
36
+ Статус определяется по `delivery` как обычно; переписку проверяй по новому UUID.
37
+ - `REFUSED` — отказ с HTTP-статусом, кодом, текстом и доступной причиной. 404 и 409
38
+ не мешают следующим диалогам. 429 останавливает пакет.
39
+ - `UNKNOWN` — 503, таймаут, сетевая ошибка, некорректная квитанция или
40
+ `delivery=UNKNOWN`. Не повторяй запуск, не меняй ключ. Сначала проверь переписку через
41
+ `besales_dialogue_transcript`; отсутствие сообщения ещё не доказывает отказ запуска.
42
+ - `NOT_PROCESSED` — запрос не отправлялся после 429. Автоматически пакет не продолжай.
43
+
44
+ STARTED не равно доставлено: смотри `delivery` у каждого. В `summary` сохранены
45
+ `started`, `refused`, `unknown`, `notProcessed`; дополнительно `notDelivered` считает
46
+ `STARTED` с `NOT_DELIVERED`, а `deliveryUnknown` — квитанции с `UNKNOWN`/`PENDING`.
47
+
48
+ Отчёт не усекается: при превышении бюджета ответа общий конверт моста возвращает
49
+ квитанцию с предупреждением вместо частичного отчёта. В будущих вызовах выбирай
50
+ меньший пакет; уже отправленный пакет целиком не повторяй.
@@ -0,0 +1,37 @@
1
+ # Повтор ответа на последнее входящее
2
+
3
+ `besales_dialogue_reprocess_last_inbound` заново запускает ответ бота на сохранённое последнее
4
+ сообщение клиента. Нового входящего в истории не появляется. Клиент получит настоящее сообщение;
5
+ генерация ответа расходует квоту. Нужен администратор с правом управления каналом и явное «да» человека.
6
+
7
+ Перед вызовом покажи человеку выбранные диалоги и последствие: бот ответит каждому клиенту.
8
+ После подтверждения передай `conversation_ids` — от одного до 50 UUID без дублей — и
9
+ `explicit_confirmation: true`. Поле `workspace_id` выбирает подключение по обычным правилам моста.
10
+ Без подтверждения, при дублях, невалидном UUID или превышении лимита ни один запрос не отправляется.
11
+
12
+ Не применяй, если клиент уже видел ответ, бот заглушён или ход идёт. Сервер проверяет эти условия,
13
+ а также более новое входящее, отказ клиента, закрытое окно переписки и остальные ограничения кнопки.
14
+ Инструмент вызывает тот же `POST /conversations/:conversationId/reprocess-last-inbound`, сохраняя
15
+ серверные правила допуска, дедупликации и брони. Область `run:conversation-reprocess` входит в `mcp:*`;
16
+ роль человека за ключом проверяется сервером. Личные ключи публичного API и интеграции не допускаются.
17
+
18
+ Запросы идут строго последовательно. В `results` для каждого диалога возвращается:
19
+
20
+ - `ENQUEUED`: исходное `messageId` и уведомление сервера. Это постановка в очередь, не доставка.
21
+ - `REFUSED`: `httpStatus`, `code` и `message` сервера. Отказы одного диалога не скрывают остальные.
22
+ - `UNKNOWN`: исход отправленного запроса неизвестен (таймаут, 5xx или неразобранное подтверждение).
23
+ - `NOT_PROCESSED`: запрос не отправлялся, потому что перед ним получен 429.
24
+
25
+ Сводка `summary` содержит `enqueued`, `refused`, `unknown`, `notProcessed`; сумма равна размеру списка.
26
+ При 429 пакет останавливается без ожидания и автоповтора, доступный срок ожидания возвращается как
27
+ `retryAfterSeconds`. Не повторяй весь пакет: уже принятые диалоги остаются принятыми.
28
+
29
+ При `UNKNOWN` сначала найди квитанцию или статус хода и проверь переписку через
30
+ `besales_dialogue_transcript`. Отсутствие ответа в истории сразу после вызова не доказывает,
31
+ что постановка не состоялась. Отдельного маршрута receipt у этой операции нет; до выяснения
32
+ исхода не повторяй мутацию автоматически. Результат пакета не усекать.
33
+ При отказе по размеру передавай меньше `conversation_ids` в следующих вызовах; уже отправленный пакет не повторяй, сначала проверь квитанции, статус хода и переписку.
34
+
35
+ Версия пакета остаётся предварительной. Выпуск — после выкладки API и серверной проверки,
36
+ по решению владельца и правилам [RELEASING.md](../RELEASING.md). Подписанную выгрузку маршрутов
37
+ перевыпускают из чистого коммита моста; локальный диагностический экспорт её не заменяет.
@@ -135,3 +135,10 @@ enumValuesByPath разворачивает refs и все ветки anyOf/oneO
135
135
  Адрес отказавшего поля API сохраняется в `error.details.path`, например `isEnabled` или
136
136
  `config.steps[0].getcourse.delivery`. Диагностика допускает только ограниченный адрес поля;
137
137
  URL, файловые пути и произвольное содержимое вместо адреса не передаются.
138
+
139
+ Предпросмотр холодного приветствия GetCourse принимает `message` вместе с `mailing_variant_id`,
140
+ включая `continuation:true`: разовый текст переопределяет текст рассылки, её настройки сохраняются.
141
+ Продолжению по-прежнему нужны файл contacts, сегмент и фактически выбранный непустой текст;
142
+ отсутствие обоих источников адресуется обоим полям. `start` и `batch_start` требуют ровно один источник.
143
+ Проверки: `getcourse-cold-welcome.spec.ts` и `scripts/getcourse-text-source.spec.mjs`; второй набор
144
+ также сохраняет контроль альтернативных путей отказа с искусственным XOR.
@@ -0,0 +1,100 @@
1
+ # Личные номера Telegram: прогрев и лесенка
2
+
3
+ Инструменты входят в профиль `platform`, читающие — также в `readonly`. Мост обращается
4
+ только к ядру Besales; пароли, сессии и ключи Telegram не принимаются и не возвращаются.
5
+ Идентификаторы канала и номера берутся из навигации и таблицы номеров.
6
+
7
+ | Инструмент `besales_platform_telegram_personal_…` | Назначение | Право |
8
+ | --- | --- | --- |
9
+ | `accounts` | Таблица номеров, сводка и листание | `read:channels` |
10
+ | `warmup` | Настройки и аналитика прогрева | `read:channels` |
11
+ | `warmup_update` | Частичная правка настроек прогрева | `manage:channels` |
12
+ | `account_warmup` | Включение и ручной отказ отдельного номера | `manage:channels` |
13
+ | `ramp` | Чтение `newAccountRampEnabled` | `read:channels` |
14
+ | `profile_bulk` | Пачечная правка профилей своих номеров: имя, описание, дата рождения, ники | `manage:channels` |
15
+ | `profile_job` | Итог задания правки профилей по номерам | `read:channels` |
16
+
17
+ У инструментов канала обязателен `channel_id`; у `account_warmup` также `account_id`
18
+ и булев `is_warming_up`. `workspace_id` можно не передавать при выбранной рабочей области.
19
+ Лишнее поле отклоняется до HTTP. Прогревом можно управлять без подтверждения, потому что
20
+ сообщения идут только между своими номерами.
21
+
22
+ Таблица принимает `search` (до 200 знаков), `status` (состояние связи), `page` (от 1)
23
+ и `limit` (1–100). Сводка `summary` относится ко всем живым номерам до фильтра;
24
+ `pagination` описывает страницу, `truncated` — неполное чтение. Мост сохраняет их целиком.
25
+ Прогрев читается отдельно в `warmup`: `active`, `reason`, `label`, `until`, `optedOut`.
26
+ Он не блокирует участие в рассылке; `campaign` и состояние строки показывают участие и разгон.
27
+ `restriction` содержит ограничения, наблюдения проверок и жалоб. `autoAppealNotBefore`
28
+ не означает запланированную подачу: автоматическая подача жалоб сейчас выключена.
29
+
30
+ Лесенка: 3, 4, 5, 6, 7 с первого дня рассылки; прогрев идёт параллельно 5 дней.
31
+ Прогрев включается сам при подключении номера, если его не выключить вручную.
32
+ `is_warming_up:false` выводит номер из автоматического прогрева до ручного включения;
33
+ true снимает ручной отказ. Чтение лесенки возвращает только её булев флаг, без других
34
+ настроек рассылки.
35
+
36
+ `warmup_update` принимает поля `TelegramWarmupPatchDto` в snake_case: `enabled`,
37
+ `images_enabled`, `messages_per_day`, `time_variation_minutes`, `response_delay_min_minutes`,
38
+ `response_delay_max_minutes`, `image_chance_percent`, `posts_to_react`,
39
+ `max_accounts_per_channel_reaction`, `pairing_hour_utc`, `hourly_interval_hours`,
40
+ `reaction_types` (до 100 строк), `reaction_hours_utc` (до 24 целых чисел).
41
+ В HTTP уходят camelCase-поля DTO. Пропуск сохраняет прежнее значение, false, 0 и []
42
+ передаются буквально. Допустимые рабочие диапазоны настроек проверяет ядро.
43
+
44
+ ## Профили номеров: `profile_bulk` и `profile_job`
45
+
46
+ `profile_bulk` меняет профили СВОИХ личных номеров канала, а не клиентов. Операция идёт
47
+ заданием фермы номеров: ответ `{job_id, state, total}` означает «принято», а не «изменено».
48
+ Итог по каждому номеру читает `profile_job` (`channel_id` и `job_id` из ответа): `state` задания
49
+ (`PENDING`, `RUNNING`, `COMPLETED`), `counters`, а у каждого номера — `state`, `failure_code` и
50
+ `outcome.fields.<поле>`. Элемент с частичным успехом остаётся `SUCCEEDED`, поэтому читай поля:
51
+ отдельное поле может быть `FAILED`. `TELEGRAM_BIO_TOO_LONG` — описание длиннее лимита Telegram
52
+ (до 70 знаков без Premium, до 140 с Premium); `PROFILE_PROVIDER_OUTCOME_UNKNOWN` — исход у
53
+ Telegram неизвестен, сначала сверь профиль номера, не повторяй вслепую.
54
+
55
+ Поля `profile_bulk` в snake_case, в HTTP уходят camelCase-поля `TelegramPersonalBulkProfileDto`:
56
+
57
+ | Вход | В теле ядра | Правило |
58
+ | --- | --- | --- |
59
+ | `account_ids` или `all:true` | `accountIds` или `all` | ровно одно; `account_ids` — 1–100 уникальных UUID v4 |
60
+ | `first_name`, `last_name` | `patch.firstName`, `patch.lastName` | до 64 знаков, без нормализации |
61
+ | `bio` | `patch.bio` | до 140 знаков; лимит Telegram по Premium проверяет ферма |
62
+ | `birthday` | `patch.birthday` | `{day, month, year?}`; `null` снимает дату |
63
+ | `usernames` | `usernames[{accountId, username}]` | ники по номерам; `account_ids` и `all` не передаются |
64
+ | `username_base` | `usernameBase` | основа ника для номеров из `account_ids` или `all` |
65
+ | `idempotency_key` | заголовок `x-idempotency-key` | обязателен: 1–200 видимых ASCII-знаков без пробелов |
66
+
67
+ Фото через MCP пока не меняется — только в приложении: поле `avatar_file_id` не принимается и
68
+ отвергается до HTTP как лишнее.
69
+
70
+ Пропуск поля сохраняет прежнее значение; пустые строки и `null` передаются буквально, а
71
+ ложное или пустое значение выбора (`all:false`, `account_ids:[]`, `usernames:[]`) отвергается до HTTP.
72
+ Вызов без единой правки (ни поля профиля, ни ников) тоже отвергается: нечего менять.
73
+
74
+ Ники — ровно один режим либо ни одного: `usernames` (по номерам) или `username_base` (общая основа).
75
+ Оба режима сразу, `usernames` вместе с `account_ids`/`all`, повтор номера или ника (без учёта
76
+ регистра) и ник вне правил Telegram (5–32 знака, латиница, цифры и `_`, с буквы, без `__`,
77
+ без `_` в конце) отвергаются до HTTP. Ники необратимо освобождают прежние ники у Telegram:
78
+ перед вызовом покажи человеку список номеров и новые значения и получи ответ «делать?».
79
+ Явный `explicit_confirmation` не принимается: так решено в плане пачки.
80
+
81
+ Ключ идемпотентности для этого маршрута ядро читает из `x-idempotency-key`, а не из общего
82
+ `Idempotency-Key`, поэтому транспорт моста отправляет его именно под этим именем (поле
83
+ `idempotencyKeyHeader` запроса). Повтор того же намерения — с тем же ключом и аргументами:
84
+ ядро вернёт прежний ответ, профиль второй раз не изменится и суточный предел смен не тратится.
85
+ При неизвестном исходе новый ключ автоматически не создавай.
86
+
87
+ Ссылка настройки номера (`…_setup_link`) в этой пачке не выпускается: маршрута
88
+ `telegram-personal-setup-sessions` в ядре нет, это отдельная задача ядра.
89
+
90
+ Ограничения, ручная жалоба и чтение её исхода описаны в
91
+ [документе апелляции](telegram-personal-restrictions.md). Перед жалобой покажи человеку
92
+ номер и текст, получи явное согласие и только затем передай `explicit_confirmation:true`.
93
+ Жалоба необратима, не чаще раза в сутки на номер; автоматическая подача выключена.
94
+
95
+ Проверки схем, запросов, профилей, строгого ввода, описаний и сохранения ответа находятся
96
+ в `telegram-personal-management.spec.ts`, `telegram-personal-restrictions.spec.ts` и
97
+ `telegram-personal-profile.spec.ts`. `scripts/telegram-personal-confirmation-mutants.mjs`
98
+ проверяет, что снятие обязательного согласия в Zod или опубликованной схеме ломает именованное
99
+ утверждение теста. `scripts/telegram-personal-profile-mutants.mjs` делает то же для запрета
100
+ двух режимов ников сразу (в Zod и в опубликованной схеме) и для имени заголовка ключа.
@@ -0,0 +1,87 @@
1
+ # Ограничения личных номеров Telegram
2
+
3
+ Все три инструмента входят в профиль `platform`. Они обращаются к API выбранного канала;
4
+ UUID номера берётся из таблицы номеров. Прямых обращений к сервису номеров в мосту нет.
5
+
6
+ | Инструмент | Назначение | Право |
7
+ | --------------------------------------------------------- | -------------------------------------------- | ----------------- |
8
+ | `besales_platform_telegram_personal_restriction_check` | Проверка через системного бота @SpamBot | `manage:channels` |
9
+ | `besales_platform_telegram_personal_restriction_appeal` | Отправка апелляции от выбранных номеров | `manage:channels` |
10
+ | `besales_platform_telegram_personal_operational_task_get` | Чтение состояния задачи и ограничений номера | `read:channels` |
11
+
12
+ В проверке и апелляции выбери ровно одну форму: `all:true` либо `account_ids` из 1–100 уникальных
13
+ UUID v4. Предел 100 относится и ко всем номерам канала: если их больше, API отклонит пакет до
14
+ постановки. `workspace_id` необязателен при однозначно выбранном пространстве.
15
+ `idempotency_key` обязателен: 1–200 видимых ASCII-символов без пробелов. Мост передаёт его в
16
+ `idempotencyKey` тела API. Заголовок `x-idempotency-key` также поддерживается ядром; разные
17
+ ключи в теле и заголовке дают 400. Для одного намерения сохраняй ключ и аргументы.
18
+
19
+ Проверка обращается только к системному боту, людям ничего не отправляет и
20
+ `explicit_confirmation` не принимает. Апелляция требует явного согласия человека и
21
+ `explicit_confirmation:true`. Перед вызовом покажи человеку номер и свой текст, если он задан (без текста система подберёт английский шаблон сама), и получи явное согласие.
22
+ Автоматическая подача сейчас выключена. Номер отправит текст @SpamBot от своего имени, отменить отправку
23
+ нельзя. `text` необязателен: без него система выбирает английский текст из пула разных
24
+ апелляций со спинтаксом: 18 шаблонов, 144 раскрытия, отдельно для каждого номера,
25
+ без одинаковых текстов внутри пакета. Формулировок об общении только по входящим обращениям нет.
26
+ Выбор сохраняется до отправки и повторяется с тем же ключом. Сохранённый удалённый или
27
+ неизвестный ID даёт 409 `IDEMPOTENCY_REQUEST_MISMATCH` до отправки всей пачки;
28
+ новый ключ автоматически не создаётся. Свой `text` — 1–500 знаков
29
+ после удаления пробелов по краям; он имеет приоритет и не раскрывается как спинтакс.
30
+ Ограничение — раз в UTC-сутки
31
+ на номер; автоматической апелляции нет.
32
+
33
+ Ответ пакетных инструментов содержит `items` с отдельным исходом каждого номера:
34
+ `QUEUED` с `taskId`, `FAILED` с `code/message` или `SKIPPED` с причиной. `APPEAL_NOT_RESTRICTED`
35
+ и `APPEAL_DAILY_LIMIT` дают `SKIPPED`. Очередь не подтверждает проверку или отправку апелляции.
36
+ Для системного текста исход номера содержит `appealTemplateId`; текст в ответе не возвращается.
37
+ Квитанции не обрезаются. При неизвестном исходе сначала прочитай задачу или доступность
38
+ номеров (`besales_get_telegram_personal_capacity`); новый ключ автоматически не создавай.
39
+ Если taskId неизвестен, capacity показывает последний результат и время проверки, но не
40
+ доказывает исход именно твоего запроса.
41
+
42
+ Чтение задачи принимает `channel_id` и `task_id`, входит также в `readonly` и возвращает
43
+ `state` (`PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`), код/текст отказа и `restrictionStatus`.
44
+ Последнее поле — текущий статус номера, прочитанный после задачи, а не исторический снимок
45
+ её исхода. Более поздние действия могли его изменить. `null` означает, что статус номера
46
+ недоступен; `SUCCEEDED` само по себе не означает отсутствие ограничений.
47
+
48
+ Примеры с вымышленными UUID:
49
+
50
+ ```json
51
+ {
52
+ "name": "besales_platform_telegram_personal_restriction_check",
53
+ "arguments": {
54
+ "channel_id": "23000000-0000-4000-8000-000000000001",
55
+ "all": true,
56
+ "idempotency_key": "example-check-1"
57
+ }
58
+ }
59
+ ```
60
+
61
+ ```json
62
+ {
63
+ "name": "besales_platform_telegram_personal_restriction_appeal",
64
+ "arguments": {
65
+ "channel_id": "23000000-0000-4000-8000-000000000001",
66
+ "account_ids": ["24000000-0000-4000-8000-000000000001"],
67
+ "explicit_confirmation": true,
68
+ "idempotency_key": "example-appeal-1"
69
+ }
70
+ }
71
+ ```
72
+
73
+ ```json
74
+ {
75
+ "name": "besales_platform_telegram_personal_operational_task_get",
76
+ "arguments": {
77
+ "channel_id": "23000000-0000-4000-8000-000000000001",
78
+ "task_id": "25000000-0000-4000-8000-000000000001"
79
+ }
80
+ }
81
+ ```
82
+
83
+ Выкладка: сначала ядро с поддержкой ключа в теле и `restrictionStatus`, затем мост после
84
+ вливания в отгружаемую ветку. В тот же день выгрузи реестр маршрутов командой
85
+ `BESALES_SPECS_CONTRACTS_DIR=/путь/к/specs/contracts BESALES_API_CONTRACTS_DIR=/путь/к/besales-api/contracts yarn publish:mcp-registry`.
86
+ В ядре выполни `yarn contracts:mcp-routes:check` и `yarn contracts:mcp-routes:check-fresh`.
87
+ До вливания моста выгрузка не выполняется; `contracts/mcp-tool-routes.json` вручную не правится.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@besales/mcp",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "Model Context Protocol server for Besales",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/be-sales/mcp#readme",
@@ -0,0 +1,110 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { beforeAll, describe, expect, it } from 'vitest';
5
+
6
+ import { getcourseColdWelcomeTools } from '../src/tools/definitions/getcourse-cold-welcome.ts';
7
+ import { diffRegistries } from './registry-diff.mjs';
8
+ import { sampleFor } from './sample-args.mjs';
9
+ import { loadShippedDefinitions } from './shipped-definitions.mjs';
10
+ import { shippedRef } from './shipped-ref.mjs';
11
+ import { mergeShippedArgs } from './shipped-sample-candidates.mjs';
12
+ import {
13
+ buildShippedSample,
14
+ filterOf,
15
+ prepareKnownArgs,
16
+ routeShape,
17
+ schemaLagExplains,
18
+ schemaPathsOf,
19
+ } from './shipped-sample.mjs';
20
+
21
+ const inputRoot = fileURLToPath(new URL('..', import.meta.url));
22
+ const inputWorkspace = '00000000-0000-4000-8000-0000000000ff';
23
+ const inputMap = JSON.parse(
24
+ readFileSync(new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url), 'utf8'),
25
+ );
26
+ const inputLocalRegistry = JSON.parse(
27
+ readFileSync(new URL('../src/schemas/mcp-tools.json', import.meta.url), 'utf8'),
28
+ );
29
+ let inputShippedRegistry;
30
+ let inputShippedDefinitions;
31
+
32
+ // Как у emit-tool-routes.spec.mjs: настоящий кэшированный git-ref, без fetch и записи Git.
33
+ beforeAll(async () => {
34
+ const commit = execFileSync('git', ['-C', inputRoot, 'rev-parse', shippedRef()], {
35
+ encoding: 'utf8',
36
+ }).trim();
37
+ inputShippedRegistry = JSON.parse(
38
+ execFileSync('git', ['-C', inputRoot, 'show', commit + ':src/schemas/mcp-tools.json'], {
39
+ encoding: 'utf8',
40
+ maxBuffer: 16 * 1024 * 1024,
41
+ }),
42
+ );
43
+ inputShippedDefinitions = (await loadShippedDefinitions(inputRoot, commit)).toolDefinitions;
44
+ }, 60000);
45
+
46
+ describe('источник текста в настоящем переборе отгруженных образцов', () => {
47
+ it.each([
48
+ { suffix: 'preview', xor: false },
49
+ { suffix: 'start', xor: false },
50
+ { suffix: 'batch_start', xor: false },
51
+ { suffix: 'preview', xor: true },
52
+ ])(
53
+ 'cold_welcome_$suffix (контроль XOR=$xor): отгруженный образец без ложного отсутствия текста',
54
+ ({ suffix, xor }) => {
55
+ const name = `besales_getcourse_cold_welcome_${suffix}`;
56
+ const definition = getcourseColdWelcomeTools.find((entry) => entry.name === name);
57
+ const shippedDefinition = inputShippedDefinitions.find((entry) => entry.name === name);
58
+ const shippedSchema = inputShippedRegistry.tools.find(
59
+ (entry) => entry.name === name,
60
+ ).inputSchema;
61
+ const localSchema = inputLocalRegistry.tools.find((entry) => entry.name === name).inputSchema;
62
+ const canonical = inputMap.find((entry) => entry.tool === name).canonicalArgs;
63
+ // Искусственный адресный XOR сохраняет регрессию W10FIX3, когда продуктовый preview уже исправлен.
64
+ const acceptV2 = filterOf(
65
+ xor
66
+ ? {
67
+ ...definition,
68
+ schema: definition.schema.superRefine((input, ctx) => {
69
+ if (
70
+ input.continuation === true &&
71
+ input.message !== undefined &&
72
+ input.mailing_variant_id !== undefined
73
+ ) {
74
+ for (const field of ['message', 'mailing_variant_id']) {
75
+ ctx.addIssue({ code: 'custom', path: [field], message: 'контроль XOR' });
76
+ }
77
+ }
78
+ }),
79
+ }
80
+ : definition,
81
+ inputWorkspace,
82
+ );
83
+ const main = acceptV2(canonical);
84
+ expect(main.ok).toBe(true);
85
+ const mainRouteShape = routeShape(main.request);
86
+ const shippedPaths = schemaPathsOf(shippedSchema, name);
87
+ const v2Paths = schemaPathsOf(localSchema, name);
88
+ const lag = diffRegistries(
89
+ inputShippedRegistry.tools,
90
+ inputLocalRegistry.tools,
91
+ ).contractLag.filter((entry) => entry.tool === name);
92
+ const known = prepareKnownArgs(canonical, shippedPaths, { acceptV2, mainRouteShape });
93
+ const outcome = buildShippedSample({
94
+ full: mergeShippedArgs(sampleFor(name, shippedSchema, false), known),
95
+ known,
96
+ acceptShipped: filterOf(shippedDefinition, inputWorkspace),
97
+ acceptV2,
98
+ explains: (path, args) => schemaLagExplains(path, args, lag, v2Paths),
99
+ shippedConstraints: shippedPaths,
100
+ goodValues: () => [],
101
+ mainRouteShape,
102
+ });
103
+ expect(outcome.absentKnown).toEqual([]);
104
+ expect(outcome.refused).toEqual(xor ? [{ path: 'message', message: 'контроль XOR' }] : []);
105
+ expect(outcome.pathless).toEqual([]);
106
+ expect(outcome.impossible).toBeUndefined();
107
+ expect(routeShape(outcome.request)).toEqual(mainRouteShape);
108
+ },
109
+ );
110
+ });
@@ -452,6 +452,9 @@ export function v2Verdict(args, acceptV2, explains) {
452
452
  }
453
453
  current = withoutPath(current, path);
454
454
  omitted.push({ path, why: 'DECLARED' });
455
+ // Пути одного отказа могут быть альтернативами: не снимаем следующий, если v2 уже принимает.
456
+ const retried = acceptV2(current);
457
+ if (retried.ok) return { request: retried.request, args: current, omitted, refused };
455
458
  }
456
459
  }
457
460
  return { pathless: ['ветка v2 не приняла образец за предел раундов снятия'], omitted, refused };
@@ -8,13 +8,19 @@ import { triggerActionArgumentSchema } from '../dist/tools/definitions/trigger-a
8
8
  import { toolDefinitions } from '../dist/tools/definitions.js';
9
9
  import { triggerActionItemSchema } from '../dist/tools/definitions/platform-setup.js';
10
10
  import { canonicalCallOperatorTaskSchema } from '../dist/tools/definitions/call-operator-task.schema.js';
11
+ import { syncCallOperatorTaskSchema } from './sync-trigger-task-schema.mjs';
11
12
 
12
13
  // Границы опубликованных полей — из контракта выбранного API checkout, не из ручного JSON.
13
14
  const contractsDir = process.env.BESALES_API_CONTRACTS_DIR?.trim() || '../besales-api/contracts';
14
15
  let callOperator;
15
16
  await openapiTS(pathToFileURL(resolve(contractsDir, 'openapi-besales-api.yaml')), {
16
17
  transform(schema) {
17
- if (schema.properties?.step_type?.enum?.includes('CALL_OPERATOR') && schema.properties.task_text && schema.properties.task_due_hours) callOperator = schema;
18
+ if (
19
+ schema.properties?.step_type?.enum?.includes('CALL_OPERATOR') &&
20
+ schema.properties.task_text &&
21
+ schema.properties.task_due_hours
22
+ )
23
+ callOperator = schema;
18
24
  },
19
25
  });
20
26
  if (!callOperator?.properties?.task_text || !callOperator.properties.task_due_hours) {
@@ -29,38 +35,7 @@ const canonical = z.toJSONSchema(canonicalCallOperatorTaskSchema, {
29
35
  target: 'draft-7',
30
36
  io: 'input',
31
37
  });
32
- for (const [name, alias] of [
33
- ['task_text', 'taskText'],
34
- ['task_due_hours', 'taskDueHours'],
35
- ]) {
36
- const property = callOperator.properties[name];
37
- const nullable = property.anyOf?.some((branch) => branch.type === 'null');
38
- const value = property.anyOf?.find((branch) => branch.type !== 'null');
39
- const runtime = canonical.properties[name].anyOf.find((branch) => branch.type !== 'null');
40
- // refine считает Unicode; JSON Schema maxLength считает те же кодовые точки.
41
- if (
42
- !nullable ||
43
- (name === 'task_text'
44
- ? value?.type !== 'string' || value.maxLength !== 500
45
- : value?.type !== runtime.type ||
46
- value.minimum !== runtime.minimum ||
47
- value.maximum !== runtime.maximum)
48
- ) {
49
- throw new Error(`CALL_OPERATOR_TASK_CONTRACT_DRIFT:${name}`);
50
- }
51
- canonical.properties[name] = {
52
- ...property,
53
- description: canonical.properties[name].description,
54
- };
55
- action.properties.callOperatorConfig.properties[alias] = {
56
- ...property,
57
- description: action.properties.callOperatorConfig.properties[alias].description,
58
- };
59
- }
60
- action.properties.config = {
61
- ...action.properties.config,
62
- properties: canonical.properties,
63
- };
38
+ syncCallOperatorTaskSchema(action, canonical, callOperator);
64
39
 
65
40
  // Публикуемые объявления берутся из исполняемой схемы: refine не обещает maxLength в JSON Schema.
66
41
  const target = new URL('../src/schemas/mcp-tools.json', import.meta.url);
@@ -73,7 +48,8 @@ for (const [name, field] of [
73
48
  const definition = toolDefinitions.find((entry) => entry.name === name);
74
49
  const schema = definition?.schema.shape?.[field];
75
50
  const tool = registry.tools.find((entry) => entry.name === name);
76
- if (!schema || !tool?.inputSchema?.properties) throw new Error(`INSTAGRAM_SCHEMA_MISSING:${name}`);
51
+ if (!schema || !tool?.inputSchema?.properties)
52
+ throw new Error(`INSTAGRAM_SCHEMA_MISSING:${name}`);
77
53
  const published = z.toJSONSchema(schema, { target: 'draft-7', io: 'input' });
78
54
  delete published.$schema;
79
55
  tool.inputSchema.properties[field] = published;
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { triggerActionItemSchema } from '../src/tools/definitions/platform-setup.js';
4
+ import { canonicalCallOperatorTaskSchema } from '../src/tools/definitions/call-operator-task.schema.js';
5
+ import { syncCallOperatorTaskSchema } from './sync-trigger-task-schema.mjs';
6
+
7
+ function schemas() {
8
+ const options = { target: 'draft-7', io: 'input' };
9
+ const action = z.toJSONSchema(triggerActionItemSchema, options);
10
+ const canonical = z.toJSONSchema(canonicalCallOperatorTaskSchema, options);
11
+ const api = structuredClone(canonical);
12
+ // Уточнение API, которого refine исполняемой схемы не объявляет в JSON Schema.
13
+ api.properties.task_text.anyOf.find((branch) => branch.type === 'string').maxLength = 500;
14
+ return { action, canonical, api };
15
+ }
16
+
17
+ describe('sync trigger CALL_OPERATOR schema', () => {
18
+ it.each(['plain', 'anyOf', 'oneOf'])('синхронизирует %s без потери null и описаний', (shape) => {
19
+ const { action, canonical, api } = schemas();
20
+ const generated = action.properties.callOperatorConfig;
21
+ const object = generated.anyOf.find((branch) => branch.type === 'object');
22
+ const originalObject = structuredClone(object);
23
+ const configBefore = structuredClone(action.properties.config);
24
+ const apiBefore = structuredClone(api);
25
+ const wrapper =
26
+ shape === 'plain'
27
+ ? object
28
+ : {
29
+ description: 'nullable config',
30
+ [shape]: [{ type: 'null' }, object],
31
+ };
32
+ action.properties.callOperatorConfig = wrapper;
33
+ syncCallOperatorTaskSchema(action, canonical, api);
34
+ expect(action.properties.callOperatorConfig).toBe(wrapper);
35
+ if (shape !== 'plain') {
36
+ expect(wrapper[shape]).toEqual([{ type: 'null' }, object]);
37
+ expect(wrapper.description).toBe('nullable config');
38
+ }
39
+ for (const [name, alias] of [
40
+ ['task_text', 'taskText'],
41
+ ['task_due_hours', 'taskDueHours'],
42
+ ]) {
43
+ expect(object.properties[alias]).toEqual({
44
+ ...api.properties[name],
45
+ description: originalObject.properties[alias].description,
46
+ });
47
+ expect(action.properties.config.properties[name]).toEqual(canonical.properties[name]);
48
+ }
49
+ expect({ ...object, properties: undefined }).toEqual({
50
+ ...originalObject,
51
+ properties: undefined,
52
+ });
53
+ expect({ ...action.properties.config, properties: undefined }).toEqual({
54
+ ...configBefore,
55
+ properties: undefined,
56
+ });
57
+ expect(api).toEqual(apiBefore);
58
+ });
59
+
60
+ it('синхронизирует фактическую nullable-схему Zod', () => {
61
+ const { action, canonical, api } = schemas();
62
+ syncCallOperatorTaskSchema(action, canonical, api);
63
+ expect(action.properties.callOperatorConfig.anyOf[1]).toEqual({
64
+ type: 'null',
65
+ });
66
+ expect(
67
+ action.properties.callOperatorConfig.anyOf[0].properties.taskText.anyOf[0].maxLength,
68
+ ).toBe(500);
69
+ });
70
+
71
+ it.each(['task_text', 'task_due_hours'])('сохраняет отказ на дрейф API: %s', (field) => {
72
+ const { action, canonical, api } = schemas();
73
+ const value = api.properties[field].anyOf.find((branch) => branch.type !== 'null');
74
+ if (field === 'task_text') value.maxLength = 501;
75
+ else value.maximum += 1;
76
+ expect(() => syncCallOperatorTaskSchema(action, canonical, api)).toThrow(
77
+ `CALL_OPERATOR_TASK_CONTRACT_DRIFT:${field}`,
78
+ );
79
+ });
80
+
81
+ it('не принимает неоднозначное объединение объектов', () => {
82
+ const { action, canonical, api } = schemas();
83
+ action.properties.callOperatorConfig.anyOf.push({
84
+ type: 'object',
85
+ properties: {},
86
+ });
87
+ expect(() => syncCallOperatorTaskSchema(action, canonical, api)).toThrow(
88
+ 'CALL_OPERATOR_CONFIG_SCHEMA_UNEXPECTED',
89
+ );
90
+ });
91
+ });
@@ -0,0 +1,20 @@
1
+ // Точечный вариант sync-api-contracts: только схема и описание событийного триггера.
2
+ // Сохраняет ручные ограничения и все прочие инструменты; типы и маршруты уже актуальны.
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { z } from 'zod';
5
+ import { toolDefinitions } from '../src/tools/definitions.ts';
6
+ import { eventTriggerDescription } from '../src/tools/definitions/trigger-upsert.tool.ts';
7
+ import { syncSchemaDescriptions } from './sync-schema-descriptions.mjs';
8
+
9
+ const registryPath = new URL('../src/schemas/mcp-tools.json', import.meta.url);
10
+ const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
11
+ const name = 'besales_trigger_upsert';
12
+ const definition = toolDefinitions.find(tool => tool.name === name);
13
+ const tool = registry.tools.find(tool => tool.name === name);
14
+ if (!definition || !tool) throw new Error(`REGISTRY_TOOL_MISSING:${name}`);
15
+ const derived = z.toJSONSchema(definition.schema, { io: 'input', unrepresentable: 'any' });
16
+ if (!derived.properties?.trigger) throw new Error('EVENT_TRIGGER_SCHEMA_MISSING');
17
+ tool.inputSchema.properties.trigger = derived.properties.trigger;
18
+ syncSchemaDescriptions(tool.inputSchema, derived);
19
+ if (!tool.description.includes(eventTriggerDescription)) tool.description += ' ' + eventTriggerDescription;
20
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');