@besales/mcp 0.33.0 → 0.37.1

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 (50) hide show
  1. package/README.md +45 -12
  2. package/dist/http/api-client.d.ts +2 -0
  3. package/dist/http/api-client.js +55 -0
  4. package/dist/http/api-client.js.map +1 -1
  5. package/dist/resources/docs/agent-behavior.md +7 -5
  6. package/dist/resources/docs/prompt-anatomy.md +4 -4
  7. package/dist/resources/docs/prompt-principles.md +22 -3
  8. package/dist/schemas/mcp-tools.json +1369 -98
  9. package/dist/skills/besales-dialogue-audit/SKILL.md +6 -0
  10. package/dist/tools/definitions/broadcast-analytics.d.ts +28 -7
  11. package/dist/tools/definitions/broadcast-analytics.js +399 -33
  12. package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
  13. package/dist/tools/definitions/broadcast-mailing-variants.d.ts +19 -0
  14. package/dist/tools/definitions/broadcast-mailing-variants.js +107 -0
  15. package/dist/tools/definitions/broadcast-mailing-variants.js.map +1 -0
  16. package/dist/tools/definitions/dialogue-audit.d.ts +23 -0
  17. package/dist/tools/definitions/dialogue-audit.js +155 -0
  18. package/dist/tools/definitions/dialogue-audit.js.map +1 -0
  19. package/dist/tools/definitions/follow-up-setup.d.ts +27 -0
  20. package/dist/tools/definitions/follow-up-setup.js +76 -0
  21. package/dist/tools/definitions/follow-up-setup.js.map +1 -0
  22. package/dist/tools/definitions/follow-up.d.ts +19 -0
  23. package/dist/tools/definitions/follow-up.js +81 -3
  24. package/dist/tools/definitions/follow-up.js.map +1 -1
  25. package/dist/tools/definitions/getcourse-wave.d.ts +147 -0
  26. package/dist/tools/definitions/getcourse-wave.js +505 -0
  27. package/dist/tools/definitions/getcourse-wave.js.map +1 -0
  28. package/dist/tools/definitions/platform-setup.d.ts +92 -20
  29. package/dist/tools/definitions/platform-setup.js +132 -537
  30. package/dist/tools/definitions/platform-setup.js.map +1 -1
  31. package/dist/tools/definitions/scheduled-tasks.d.ts +9 -0
  32. package/dist/tools/definitions/scheduled-tasks.js +49 -2
  33. package/dist/tools/definitions/scheduled-tasks.js.map +1 -1
  34. package/dist/tools/definitions/session.js +6 -4
  35. package/dist/tools/definitions/session.js.map +1 -1
  36. package/dist/tools/definitions/shared.d.ts +75 -13
  37. package/dist/tools/definitions/shared.js +120 -7
  38. package/dist/tools/definitions/shared.js.map +1 -1
  39. package/dist/tools/definitions.d.ts +171 -26
  40. package/dist/tools/definitions.js +4 -0
  41. package/dist/tools/definitions.js.map +1 -1
  42. package/dist/tools/registry-mapping-cases-core.d.ts +38 -0
  43. package/dist/tools/registry-mapping-cases-core.js +700 -0
  44. package/dist/tools/registry-mapping-cases-core.js.map +1 -0
  45. package/dist/tools/registry-mapping-cases-platform.d.ts +2 -0
  46. package/dist/tools/registry-mapping-cases-platform.js +954 -0
  47. package/dist/tools/registry-mapping-cases-platform.js.map +1 -0
  48. package/dist/tools/registry.js +12 -6
  49. package/dist/tools/registry.js.map +1 -1
  50. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { refineFollowUpConfig } from './follow-up.js';
3
- import { agentOutputConfigSchema, agentRuntimeConfigSchema, agentScopeSchema, agentToolPatchSchema, anyObjectSchema, behaviorErrorHandlingSchema, behaviorGreetingSchema, behaviorMediaHandlingSchema, behaviorMessageFilterSchema, behaviorMessageSplittingSchema, behaviorOperatorScheduleSchema, behaviorPrivacySchema, behaviorTypingBehaviorSchema, behaviorUrlHandlingSchema, compactObject, defineTool, feedbackSheetRowSchema, followUpSequenceTypeSchema, followUpTriggerSchema, mapBehaviorErrorHandlingInput, mapBehaviorGreetingInput, mapBehaviorMediaHandlingInput, mapBehaviorOperatorScheduleInput, mapBehaviorTypingInput, mapBehaviorUrlHandlingInput, mapOutputConfigInput, mapPipelineAutoFinishInput, mapPipelineCrmBehaviorInput, mapPlatformAccessControlInput, mapPlatformBitrix24BotSettingsInput, mapPlatformCrmBehaviorInput, mapPlatformGeneralSettingsInput, mapPlatformGetcourseSettingsInput, mapPlatformInstagramCommentInput, mapPlatformSalebotSettingsInput, mapPlatformSmsgorodSettingsInput, mapPlatformVariablesConfigInput, mapPlatformWidgetMarketingInput, mapRuntimeConfigInput, modelLaneSchema, pipelineAutoFinishSchema, pipelineCrmBehaviorSchema, platformAccessControlSchema, platformBitrix24BotSettingsSchema, platformCrmBehaviorSchema, platformGeneralSettingsSchema, platformGetcourseSettingsSchema, platformInstagramCommentSchema, platformSalebotSettingsSchema, platformSetupConfirmationSchema, platformSmsgorodSettingsSchema, platformVariablesConfigSchema, platformWidgetMarketingSchema, routingRuleConditionSchema, routingRuleLeadStatusSchema, routingStrategySchema, setupConfirmation, triggerScopeSchema, uuidSchema, withInstructions, } from './shared.js';
2
+ import { followUpSetupTools } from './follow-up-setup.js';
3
+ import { getCourseWaveTools } from './getcourse-wave.js';
4
+ import { agentOutputConfigSchema, agentRuntimeConfigSchema, agentScopeSchema, agentToolPatchSchema, anyObjectSchema, behaviorErrorHandlingSchema, behaviorGreetingSchema, behaviorMediaHandlingSchema, behaviorMessageFilterSchema, behaviorMessageSplittingSchema, behaviorOperatorScheduleSchema, behaviorPrivacySchema, behaviorTypingBehaviorSchema, behaviorUrlHandlingSchema, buildListQuery, compactObject, defineTool, feedbackSheetRowSchema, mapBehaviorErrorHandlingInput, mapBehaviorGreetingInput, mapBehaviorMediaHandlingInput, mapBehaviorOperatorScheduleInput, mapBehaviorTypingInput, mapBehaviorUrlHandlingInput, mapOutputConfigInput, mapPipelineAutoFinishInput, mapPipelineCrmBehaviorInput, mapPlatformAccessControlInput, mapPlatformBitrix24BotSettingsInput, mapPlatformCrmBehaviorInput, mapPlatformGeneralSettingsInput, mapPlatformGetcourseSettingsInput, mapPlatformInstagramCommentInput, mapPlatformSalebotSettingsInput, mapPlatformSmsgorodSettingsInput, mapPlatformVariablesConfigInput, mapPlatformWidgetMarketingInput, mapRuntimeConfigInput, modelLaneSchema, pipelineAutoFinishSchema, pipelineCrmBehaviorSchema, platformAccessControlSchema, platformBitrix24BotSettingsSchema, platformCrmBehaviorSchema, platformGeneralSettingsSchema, platformGetcourseSettingsSchema, platformInstagramCommentSchema, platformSalebotSettingsSchema, platformSetupConfirmationSchema, platformSmsgorodSettingsSchema, platformVariablesConfigSchema, mapPromptPatchOperationInput, platformWidgetMarketingSchema, PROMPT_BATCH_IDEMPOTENCY_KEY_MAX_LENGTH, PROMPT_BATCH_MAX_AGENTS, PROMPT_DIFF_MAX_LINES_LIMIT, promptPatchOperationSchema, routingRuleConditionSchema, routingRuleLeadStatusSchema, routingStrategySchema, setupConfirmation, triggerScopeSchema, uuidSchema, withInstructions, } from './shared.js';
4
5
  export const platformSetupTools = [
5
6
  defineTool({
6
7
  name: 'besales_workspace_overview',
@@ -79,484 +80,7 @@ export const platformSetupTools = [
79
80
  'Используй department.id как target_department_id в besales_platform_settings_update (getcourse_settings) для department-routing.',
80
81
  ]),
81
82
  }),
82
- defineTool({
83
- name: 'besales_getcourse_wave_import',
84
- requiredFields: ['platform_id', 'crm_id', 'channels', 'contacts'],
85
- defaultWorkspaceId: true,
86
- schema: z.object({
87
- workspace_id: uuidSchema.optional(),
88
- platform_id: uuidSchema,
89
- crm_id: uuidSchema,
90
- launch_type: z.enum(['status', 'board', 'group']).optional(),
91
- channels: z.array(z.string()).min(1),
92
- funnel: z.string().max(200).optional(),
93
- segment: z.string().max(200).optional(),
94
- cohort_label: z.string().max(200).optional(),
95
- message_variant: z.string().max(120).optional(),
96
- found_count: z.number().int().min(0).max(10_000_000).optional(),
97
- product_code: z
98
- .string()
99
- .trim()
100
- .max(100)
101
- .regex(/^[^\s/]+$/, 'product_code: без пробелов и «/»')
102
- .optional(),
103
- product_name: z.string().max(200).optional(),
104
- deal_fields: z
105
- .record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
106
- .optional(),
107
- pipeline_id: z.number().int().positive().optional(),
108
- status_id: z.number().int().positive().optional(),
109
- group_id: z.number().int().positive().optional(),
110
- contacts: z
111
- .array(z.object({
112
- contact_id: z.string(),
113
- deal_id: z.string().optional(),
114
- transport: z.string().optional(),
115
- dialog_id: z.string().optional(),
116
- lead_last_active_at: z.string().datetime({ offset: true }).optional(),
117
- segment_entered_at: z.string().datetime({ offset: true }).optional(),
118
- }))
119
- .min(1)
120
- .max(500),
121
- confirm: z.boolean().optional(),
122
- check_messenger_presence: z.boolean().optional(),
123
- dedup_window_days: z.number().int().min(0).max(365).optional(),
124
- dedup_scope: z.enum(['any', 'same_funnel']).optional(),
125
- preflight: z
126
- .object({
127
- enabled: z.boolean().optional(),
128
- product_title: z.string().max(500).optional(),
129
- product_match_key: z.string().max(500).optional(),
130
- offer_id: z.number().int().positive().optional(),
131
- other_product_active_deal_mode: z.enum(['warn', 'block', 'ignore']).optional(),
132
- })
133
- .optional(),
134
- }),
135
- createRequest: (input) => ({
136
- method: 'POST',
137
- path: `/workspaces/${input.workspace_id}/getcourse-waves/import`,
138
- body: compactObject({
139
- platformId: input.platform_id,
140
- crmId: input.crm_id,
141
- launchType: input.launch_type,
142
- channels: input.channels,
143
- funnel: input.funnel,
144
- segment: input.segment,
145
- cohortLabel: input.cohort_label,
146
- messageVariant: input.message_variant,
147
- foundCount: input.found_count,
148
- productCode: input.product_code,
149
- productName: input.product_name,
150
- dealFields: input.deal_fields,
151
- pipelineId: input.pipeline_id,
152
- statusId: input.status_id,
153
- groupId: input.group_id,
154
- dedupWindowDays: input.dedup_window_days,
155
- dedupScope: input.dedup_scope,
156
- contacts: input.contacts?.map((c) => compactObject({
157
- contactId: c.contact_id,
158
- dealId: c.deal_id,
159
- transport: c.transport,
160
- dialogId: c.dialog_id,
161
- leadLastActiveAt: c.lead_last_active_at,
162
- segmentEnteredAt: c.segment_entered_at,
163
- })),
164
- confirm: input.confirm,
165
- checkMessengerPresence: input.check_messenger_presence,
166
- preflight: input.preflight
167
- ? compactObject({
168
- enabled: input.preflight.enabled,
169
- productTitle: input.preflight.product_title,
170
- productMatchKey: input.preflight.product_match_key,
171
- offerId: input.preflight.offer_id,
172
- otherProductActiveDealMode: input.preflight.other_product_active_deal_mode,
173
- })
174
- : undefined,
175
- }),
176
- }),
177
- enhanceResponse: (response) => withInstructions(response, [
178
- 'Запуск outbound-волны GetCourse: Шаг 1 (импорт контактов + пин сделок) → Шаг 2 (отправка). По умолчанию launch_type=status (простановка полей → «Процесс» GetCourse шлёт welcome).',
179
- 'БЕЗ confirm=true — DRY-RUN: вернётся summary (totalContacts/withDeal/channels/dealFields/sample), НИЧЕГО не отправляется. Покажи summary пользователю и запускай реально только с confirm=true.',
180
- 'channels — значения GetCourseTransport: "VK" | "Telegram" | "MAX" | "SMS" | "Email" | "Site" (для аналитики «Канал волны»).',
181
- 'funnel — канонический ключ/название БИЗНЕС-воронки. Не передавай сюда имя файла, batch/resend или дату запуска. cohort_label — человекочитаемое имя конкретного запуска волны.',
182
- 'contacts[].segment_entered_at — точный ISO-момент попадания контакта в сегмент. contacts[].lead_last_active_at — только legacy/последняя активность; не используй её как замену segment_entered_at.',
183
- 'launch_type=status требует deal_fields = { "<числовой fieldId GetCourse>": значение } (одинаковые на всю волну). board требует pipeline_id+status_id, group — group_id.',
184
- 'preflight (default enabled=true) — pre-send гейт перед каждой строкой Шага 2: блокирует контакт, если уже есть активная/оплаченная сделка по этому продукту или проверка GetCourse упала. Передавай product_title/product_match_key или funnel; если контекста продукта нет вообще, backend отклонит запуск до создания job.',
185
- 'В ответе confirm-запуска — waveJobId. Опрашивай его через besales_getcourse_wave_status до status=COMPLETED (Шаг 2 идёт медленно, с паузами).',
186
- 'check_messenger_presence (default TRUE): на Шаге 1 отсеивает контакты без мессенджера (Telegram/Max/VK) — они не импортируются и не уходят в волну (число — в contactsSkippedNoMessenger). Проверка = API-вызов на КАЖДЫЙ контакт, запуск синхронно ждёт Шаг 1 → для БОЛЬШИХ когорт (сотни контактов) возможен таймаут: тогда ставь check_messenger_presence=false и сначала прогони backfill POST /api/v2/.../chat-users/import/getcourse-refresh-messengers.',
187
- 'dedup_window_days (default 30, 0–365) и dedup_scope (default any | same_funnel): any учитывает любую прошлую волну, same_funnel — ту же каноническую бизнес-воронку (legacy batch/resend-суффиксы игнорируются). 0 = выключить дедуп полностью — только для осознанного повторного запуска.',
188
- ]),
189
- }),
190
- defineTool({
191
- name: 'besales_getcourse_wave_status',
192
- requiredFields: ['job_id'],
193
- defaultWorkspaceId: true,
194
- schema: z.object({
195
- workspace_id: uuidSchema.optional(),
196
- job_id: uuidSchema,
197
- }),
198
- createRequest: (input) => ({
199
- method: 'GET',
200
- path: `/workspaces/${input.workspace_id}/getcourse-waves/${input.job_id}`,
201
- }),
202
- enhanceResponse: (response) => withInstructions(response, [
203
- 'Статус волны (джоб Шага 2): status (PROCESSING|COMPLETED), processedRows/successCount/failedCount, errorReasons (первые построчные ошибки), preflight aggregates.',
204
- 'COMPLETED + failedCount>0 — часть контактов не прошла (см. errorReasons: нет сделки, поле вне «Предложения» заказа и т.п.).',
205
- 'preflight.blocked/errors — контакты, которым НЕ отправляли; BroadcastRecipient для них не создаётся, поэтому received в аналитике не раздувается.',
206
- ]),
207
- }),
208
- defineTool({
209
- name: 'besales_getcourse_wave_run_start',
210
- requiredFields: ['platform_id', 'crm_id', 'channels', 'contacts'],
211
- defaultWorkspaceId: true,
212
- schema: z
213
- .object({
214
- workspace_id: uuidSchema.optional(),
215
- platform_id: uuidSchema,
216
- crm_id: uuidSchema,
217
- launch_type: z.literal('status').optional(),
218
- channels: z.array(z.string()).min(1),
219
- funnel: z.string().max(200).optional(),
220
- segment: z.string().max(200).optional(),
221
- cohort_label: z.string().max(200).optional(),
222
- message_variant: z.string().max(120).optional(),
223
- found_count: z.number().int().min(0).max(10_000_000).optional(),
224
- product_code: z
225
- .string()
226
- .trim()
227
- .max(100)
228
- .regex(/^[^\s/]+$/, 'product_code: без пробелов и «/»')
229
- .optional(),
230
- product_name: z.string().max(200).optional(),
231
- deal_fields: z
232
- .record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()]))
233
- .optional(),
234
- contacts: z
235
- .array(z.object({
236
- contact_id: z.string(),
237
- deal_id: z.string().optional(),
238
- transport: z.string().optional(),
239
- dialog_id: z.string().optional(),
240
- lead_last_active_at: z.string().datetime({ offset: true }).optional(),
241
- segment_entered_at: z.string().datetime({ offset: true }).optional(),
242
- }))
243
- .min(1)
244
- .max(5000),
245
- confirm: z.boolean().optional(),
246
- check_messenger_presence: z.boolean().optional(),
247
- dedup_window_days: z.number().int().min(0).max(365).optional(),
248
- dedup_scope: z.enum(['any', 'same_funnel']).optional(),
249
- preflight: z
250
- .object({
251
- enabled: z.boolean().optional(),
252
- product_title: z.string().max(500).optional(),
253
- product_match_key: z.string().max(500).optional(),
254
- offer_id: z.number().int().positive().optional(),
255
- other_product_active_deal_mode: z.enum(['warn', 'block', 'ignore']).optional(),
256
- })
257
- .optional(),
258
- chunk_size: z.number().int().min(1).max(500).optional(),
259
- explicit_confirmation: z.literal(true).optional(),
260
- confirmation_reason: z.string().optional(),
261
- })
262
- .refine((input) => !input.confirm || input.explicit_confirmation === true, {
263
- message: 'Реальный запуск требует explicit_confirmation=true: отправка волны необратима',
264
- path: ['explicit_confirmation'],
265
- }),
266
- createRequest: (input) => ({
267
- method: 'POST',
268
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs`,
269
- body: compactObject({
270
- platformId: input.platform_id,
271
- crmId: input.crm_id,
272
- launchType: input.launch_type,
273
- channels: input.channels,
274
- funnel: input.funnel,
275
- segment: input.segment,
276
- cohortLabel: input.cohort_label,
277
- messageVariant: input.message_variant,
278
- foundCount: input.found_count,
279
- productCode: input.product_code,
280
- productName: input.product_name,
281
- dealFields: input.deal_fields,
282
- contacts: input.contacts?.map((contact) => compactObject({
283
- contactId: contact.contact_id,
284
- dealId: contact.deal_id,
285
- transport: contact.transport,
286
- dialogId: contact.dialog_id,
287
- leadLastActiveAt: contact.lead_last_active_at,
288
- segmentEnteredAt: contact.segment_entered_at,
289
- })),
290
- confirm: input.confirm,
291
- checkMessengerPresence: input.check_messenger_presence,
292
- dedupWindowDays: input.dedup_window_days,
293
- dedupScope: input.dedup_scope,
294
- preflight: input.preflight
295
- ? compactObject({
296
- enabled: input.preflight.enabled,
297
- productTitle: input.preflight.product_title,
298
- productMatchKey: input.preflight.product_match_key,
299
- offerId: input.preflight.offer_id,
300
- otherProductActiveDealMode: input.preflight.other_product_active_deal_mode,
301
- })
302
- : undefined,
303
- chunkSize: input.chunk_size,
304
- }),
305
- }),
306
- enhanceResponse: (response) => withInstructions(response, [
307
- 'Durable-прогон сам режет когорту на куски и отправляет их последовательно, сам ждёт освобождения лока импорта и переживает рестарт сервиса. Опрашивай ОДИН прогон через besales_getcourse_wave_run_status, не собирай волну вручную.',
308
- 'БЕЗ confirm=true — DRY-RUN: возвращается проверка, но прогон НЕ создаётся и ничего не отправляется.',
309
- 'Реальный запуск требует ОДНОВРЕМЕННО confirm=true и explicit_confirmation=true. explicit_confirmation — MCP-гейт явного согласия оператора и в HTTP-запрос не передаётся.',
310
- 'Поддерживается ТОЛЬКО launch_type=status. Для board/group используй старый besales_getcourse_wave_import.',
311
- 'HTTP 409 означает, что на платформе уже идёт активный прогон: в ответе есть его runId — не создавай новую волну, опрашивай этот runId.',
312
- 'Статус прогона отдаёт только агрегаты. Полный построчный разбор получай через besales_getcourse_wave_run_contacts с курсорной пагинацией.',
313
- 'Исходы контактов: SENT — отправлено; SKIPPED — дедуп, нет мессенджера или preflight заблокировал; FAILED — построчная ошибка; UNKNOWN — исход НЕИЗВЕСТЕН: кусок был прерван, но отправка могла состояться.',
314
- 'UNKNOWN и FAILED НИКОГДА не переотправляются автоматически. Повторная отправка таких контактов — только осознанное решение оператора отдельной волной: отсутствие строки получателя не доказывает отсутствие отправки.',
315
- ]),
316
- }),
317
- defineTool({
318
- name: 'besales_getcourse_wave_run_status',
319
- requiredFields: ['run_id'],
320
- defaultWorkspaceId: true,
321
- schema: z.object({
322
- workspace_id: uuidSchema.optional(),
323
- run_id: uuidSchema,
324
- }),
325
- createRequest: (input) => ({
326
- method: 'GET',
327
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs/${input.run_id}`,
328
- }),
329
- enhanceResponse: (response) => withInstructions(response, [
330
- 'Это единый агрегированный статус durable-прогона: он сам последовательно обрабатывает куски, ждёт лок импорта и продолжает после рестарта сервиса. Не собирай и не запускай куски вручную.',
331
- 'Статус содержит только агрегаты. Для полного построчного разбора используй besales_getcourse_wave_run_contacts с курсорной пагинацией.',
332
- 'SENT — отправлено; SKIPPED — дедуп, нет мессенджера или блокировка preflight; FAILED — построчная ошибка; UNKNOWN — исход НЕИЗВЕСТЕН после прерванного куска, отправка могла состояться.',
333
- 'UNKNOWN и FAILED НИКОГДА не переотправляются автоматически. Решение о повторе принимает оператор и запускает отдельную волну, потому что отсутствие строки получателя не доказывает отсутствие отправки.',
334
- ]),
335
- }),
336
- defineTool({
337
- name: 'besales_getcourse_wave_run_contacts',
338
- requiredFields: ['run_id'],
339
- defaultWorkspaceId: true,
340
- schema: z.object({
341
- workspace_id: uuidSchema.optional(),
342
- run_id: uuidSchema,
343
- status: z
344
- .enum(['PENDING', 'CLAIMED', 'SENT', 'SKIPPED', 'FAILED', 'UNKNOWN'])
345
- .optional(),
346
- cursor: uuidSchema.optional(),
347
- limit: z.number().int().min(1).max(500).optional(),
348
- }),
349
- createRequest: (input) => {
350
- const params = new URLSearchParams();
351
- if (input.status)
352
- params.set('status', input.status);
353
- if (input.cursor)
354
- params.set('cursor', input.cursor);
355
- if (input.limit !== undefined)
356
- params.set('limit', String(input.limit));
357
- const query = params.toString();
358
- return {
359
- method: 'GET',
360
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs/${input.run_id}/contacts${query ? `?${query}` : ''}`,
361
- };
362
- },
363
- enhanceResponse: (response) => withInstructions(response, [
364
- 'Полный построчный разбор durable-прогона. Продолжай курсорную пагинацию по nextCursor до конца; агрегированный besales_getcourse_wave_run_status строк контактов не возвращает.',
365
- 'Фильтр status: PENDING | CLAIMED | SENT | SKIPPED | FAILED | UNKNOWN. SKIPPED означает дедуп, отсутствие мессенджера или блокировку preflight; FAILED — построчную ошибку.',
366
- 'UNKNOWN означает, что исход НЕИЗВЕСТЕН: кусок был прерван, но отправка могла состояться. UNKNOWN и FAILED НИКОГДА не переотправляются автоматически.',
367
- 'Повтор UNKNOWN/FAILED допустим только осознанным решением оператора отдельной волной: отсутствие строки получателя не доказывает отсутствие отправки.',
368
- ]),
369
- }),
370
- defineTool({
371
- name: 'besales_getcourse_wave_run_cancel',
372
- requiredFields: ['run_id'],
373
- defaultWorkspaceId: true,
374
- schema: z.object({
375
- workspace_id: uuidSchema.optional(),
376
- run_id: uuidSchema,
377
- }),
378
- createRequest: (input) => ({
379
- method: 'POST',
380
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs/${input.run_id}/cancel`,
381
- }),
382
- enhanceResponse: (response) => withInstructions(response, [
383
- 'Отмена запрещает прогону заявлять новые куски. Уже заявленный кусок может завершиться; после отмены опроси besales_getcourse_wave_run_status.',
384
- 'Для полного построчного результата используй besales_getcourse_wave_run_contacts с курсорной пагинацией; статус прогона отдаёт только агрегаты.',
385
- 'UNKNOWN — исход НЕИЗВЕСТЕН после прерванного куска, отправка могла состояться. UNKNOWN и FAILED автоматически не переотправляются; повтор возможен только отдельной волной по осознанному решению оператора.',
386
- ]),
387
- }),
388
- defineTool({
389
- name: 'besales_getcourse_wave_run_batch_start',
390
- requiredFields: ['batch_key', 'platform_id', 'crm_id', 'segments'],
391
- defaultWorkspaceId: true,
392
- schema: z
393
- .object({
394
- workspace_id: uuidSchema.optional(),
395
- batch_key: z.string(),
396
- platform_id: uuidSchema,
397
- crm_id: uuidSchema,
398
- segments: z
399
- .array(z
400
- .object({
401
- mode: z.literal('status'),
402
- contacts: z
403
- .array(z.object({
404
- contact_id: z.string(),
405
- deal_id: z.string().optional(),
406
- transport: z.string().optional(),
407
- dialog_id: z.string().optional(),
408
- lead_last_active_at: z.string().datetime({ offset: true }).optional(),
409
- segment_entered_at: z.string().datetime({ offset: true }).optional(),
410
- }))
411
- .min(1)
412
- .max(2000),
413
- channels: z.array(z.string()).min(1),
414
- deal_fields: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])),
415
- funnel: z.string().max(200).optional(),
416
- segment: z.string().max(200).optional(),
417
- cohort_label: z.string().max(200).optional(),
418
- message_variant: z.string().max(120).optional(),
419
- found_count: z.number().int().min(0).max(10_000_000).optional(),
420
- product_code: z
421
- .string()
422
- .trim()
423
- .max(100)
424
- .regex(/^[^\s/]+$/, 'product_code: без пробелов и «/»')
425
- .optional(),
426
- product_name: z.string().max(200).optional(),
427
- preflight: z
428
- .object({
429
- enabled: z.boolean().optional(),
430
- product_title: z.string().max(500).optional(),
431
- product_match_key: z.string().max(500).optional(),
432
- offer_id: z.number().int().positive().optional(),
433
- other_product_active_deal_mode: z.enum(['warn', 'block', 'ignore']).optional(),
434
- })
435
- .optional(),
436
- check_messenger_presence: z.boolean().optional(),
437
- dedup_window_days: z.number().int().min(1).max(365).optional(),
438
- dedup_scope: z.enum(['any', 'same_funnel']).optional(),
439
- chunk_size: z.number().int().min(1).max(500).optional(),
440
- })
441
- .strict())
442
- .min(1)
443
- .max(10)
444
- .refine((segments) => segments.reduce((total, segment) => total + segment.contacts.length, 0) <= 5000, {
445
- message: 'В батче может быть не более 5000 контактов суммарно',
446
- }),
447
- confirm: z.boolean().optional(),
448
- explicit_confirmation: z.literal(true).optional(),
449
- confirmation_reason: z.string().optional(),
450
- })
451
- .refine((input) => !input.confirm || input.explicit_confirmation === true, {
452
- message: 'Реальный запуск требует explicit_confirmation=true: отправка волны необратима',
453
- path: ['explicit_confirmation'],
454
- }),
455
- createRequest: (input) => ({
456
- method: 'POST',
457
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs/batch`,
458
- body: compactObject({
459
- batchKey: input.batch_key,
460
- platformId: input.platform_id,
461
- crmId: input.crm_id,
462
- segments: input.segments.map((segment) => compactObject({
463
- mode: segment.mode,
464
- contacts: segment.contacts.map((contact) => compactObject({
465
- contactId: contact.contact_id,
466
- dealId: contact.deal_id,
467
- transport: contact.transport,
468
- dialogId: contact.dialog_id,
469
- leadLastActiveAt: contact.lead_last_active_at,
470
- segmentEnteredAt: contact.segment_entered_at,
471
- })),
472
- channels: segment.channels,
473
- dealFields: segment.deal_fields,
474
- funnel: segment.funnel,
475
- segment: segment.segment,
476
- cohortLabel: segment.cohort_label,
477
- messageVariant: segment.message_variant,
478
- foundCount: segment.found_count,
479
- productCode: segment.product_code,
480
- productName: segment.product_name,
481
- preflight: segment.preflight
482
- ? compactObject({
483
- enabled: segment.preflight.enabled,
484
- productTitle: segment.preflight.product_title,
485
- productMatchKey: segment.preflight.product_match_key,
486
- offerId: segment.preflight.offer_id,
487
- otherProductActiveDealMode: segment.preflight.other_product_active_deal_mode,
488
- })
489
- : undefined,
490
- checkMessengerPresence: segment.check_messenger_presence,
491
- dedupWindowDays: segment.dedup_window_days,
492
- dedupScope: segment.dedup_scope,
493
- chunkSize: segment.chunk_size,
494
- })),
495
- confirm: input.confirm,
496
- }),
497
- }),
498
- enhanceResponse: (response) => withInstructions(response, [
499
- 'Очередь исполняет сегменты ПОСЛЕДОВАТЕЛЬНО: в один момент на платформе идёт только один прогон. Опрашивай ОДИН besales_getcourse_wave_run_batch_status, не собирай сегменты и их статусы вручную.',
500
- 'batch_key сгенерируй ОДИН раз на попытку и ПЕРЕИСПОЛЬЗУЙ при ретрае: это ключ идемпотентности. Повтор с тем же ключом вернёт существующий батч; новый ключ создаст ВТОРУЮ копию кампании, которую дедуп не остановит.',
501
- 'Если повторить тот же batch_key с ИЗМЕНЁННЫМ составом сегментов, сервер вернёт HTTP 409 «ключ занят другим составом». Не меняй состав при ретрае — сначала прочитай статус существующего батча.',
502
- 'HTTP 400 отклоняет батч до запуска, если контакт есть в двух сегментах, пересекается с незавершённым прогоном платформы или ему уже отправляли за последние 30 дней. В ответе будут пары contactId + сегменты либо конфликтующий runId; исправь состав, а не обходи проверку новым ключом.',
503
- 'Два подряд отказавших сегмента ставят батч на паузу. Продолжай только через besales_getcourse_wave_run_batch_resume после проверки причин отказов.',
504
- 'UNKNOWN и FAILED НИКОГДА не переотправляются автоматически. Повтор возможен только отдельным осознанным запуском оператора.',
505
- 'БЕЗ confirm=true — DRY-RUN: батч не создаётся и ничего не отправляется. Реальный запуск дополнительно требует MCP-only explicit_confirmation=true; это поле в HTTP-запрос не передаётся.',
506
- ]),
507
- }),
508
- defineTool({
509
- name: 'besales_getcourse_wave_run_batch_status',
510
- requiredFields: ['batch_id'],
511
- defaultWorkspaceId: true,
512
- schema: z.object({
513
- workspace_id: uuidSchema.optional(),
514
- batch_id: uuidSchema,
515
- }),
516
- createRequest: (input) => ({
517
- method: 'GET',
518
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs/batches/${input.batch_id}`,
519
- }),
520
- enhanceResponse: (response) => withInstructions(response, [
521
- 'Это ЕДИНСТВЕННЫЙ агрегированный статус очереди: она запускает сегменты последовательно и держит только один прогон на платформу. Опрашивай этот batch-status, а не статусы сегментов вручную.',
522
- 'Если батч на паузе после двух подряд отказавших сегментов, сначала проверь причины, затем снимай паузу только besales_getcourse_wave_run_batch_resume.',
523
- 'UNKNOWN и FAILED никогда не переотправляются автоматически; отсутствие подтверждённого исхода не означает, что отправки не было.',
524
- ]),
525
- }),
526
- defineTool({
527
- name: 'besales_getcourse_wave_run_batch_cancel',
528
- requiredFields: ['batch_id'],
529
- defaultWorkspaceId: true,
530
- schema: z.object({
531
- workspace_id: uuidSchema.optional(),
532
- batch_id: uuidSchema,
533
- }),
534
- createRequest: (input) => ({
535
- method: 'POST',
536
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs/batches/${input.batch_id}/cancel`,
537
- }),
538
- enhanceResponse: (response) => withInstructions(response, [
539
- 'Отмена останавливает очередь батча; после неё опроси ОДИН besales_getcourse_wave_run_batch_status, не собирая сегменты вручную.',
540
- 'UNKNOWN и FAILED автоматически не переотправляются. Любой повтор — только отдельное осознанное решение оператора.',
541
- ]),
542
- }),
543
- defineTool({
544
- name: 'besales_getcourse_wave_run_batch_resume',
545
- requiredFields: ['batch_id'],
546
- defaultWorkspaceId: true,
547
- schema: z.object({
548
- workspace_id: uuidSchema.optional(),
549
- batch_id: uuidSchema,
550
- }),
551
- createRequest: (input) => ({
552
- method: 'POST',
553
- path: `/workspaces/${input.workspace_id}/getcourse-wave-runs/batches/${input.batch_id}/resume`,
554
- }),
555
- enhanceResponse: (response) => withInstructions(response, [
556
- 'Снимает паузу батча, которую вызывают два подряд отказавших сегмента. После resume опрашивай ОДИН besales_getcourse_wave_run_batch_status: очередь продолжит сегменты последовательно, один прогон на платформу.',
557
- 'UNKNOWN и FAILED никогда не переотправляются автоматически; повтор таких контактов — только отдельная осознанная волна.',
558
- ]),
559
- }),
83
+ ...getCourseWaveTools,
560
84
  defineTool({
561
85
  name: 'besales_wazzup_templates_list',
562
86
  requiredFields: ['platform_id'],
@@ -884,7 +408,16 @@ export const platformSetupTools = [
884
408
  conditionLogic: input.condition_logic,
885
409
  conditions: input.conditions?.map((condition) => ({
886
410
  customVariableId: condition.custom_variable_id,
887
- variableValue: condition.variable_value,
411
+ // Набор — источник истины; legacy-скаляр пробрасываем как есть (API примет обе формы).
412
+ // Без этого маппинга variable_values терялся бы молча: до правки сюда уходил только скаляр.
413
+ // Незаданную форму не отправляем вовсе: явный undefined в теле выглядел бы как
414
+ // «поле передано пустым» и мешал бы взаимоисключающей валидации на стороне API.
415
+ ...(condition.variable_values !== undefined
416
+ ? { variableValues: condition.variable_values }
417
+ : {}),
418
+ ...(condition.variable_value !== undefined
419
+ ? { variableValue: condition.variable_value }
420
+ : {}),
888
421
  })),
889
422
  customRouterPrompt: input.custom_router_prompt,
890
423
  isEnabled: input.is_enabled,
@@ -913,6 +446,14 @@ export const platformSetupTools = [
913
446
  is_enabled: z.boolean().optional(),
914
447
  priority: z.number().int().min(1).max(100).optional(),
915
448
  stop_propagation: z.boolean().optional(),
449
+ /** Повтор ВНУТРИ одного ответа модели. Горизонт — этот ответ, дальше состояние теряется. */
450
+ allow_repeated_calls: z.boolean().optional(),
451
+ /**
452
+ * Одно срабатывание на контакт НАВСЕГДА (право в `agent_trigger_contact_claims`).
453
+ * Ось другая, чем у `allow_repeated_calls`: тот живёт один ответ, этот переживает
454
+ * все следующие сообщения. Для действий, пишущих в чужие системы (заметка в CRM).
455
+ */
456
+ run_once_per_contact: z.boolean().optional(),
916
457
  scope: triggerScopeSchema,
917
458
  metadata: anyObjectSchema.optional(),
918
459
  actions: z.array(anyObjectSchema).optional(),
@@ -930,6 +471,8 @@ export const platformSetupTools = [
930
471
  isEnabled: input.is_enabled,
931
472
  priority: input.priority,
932
473
  stopPropagation: input.stop_propagation,
474
+ allowRepeatedCalls: input.allow_repeated_calls,
475
+ runOncePerContact: input.run_once_per_contact,
933
476
  scope: input.scope,
934
477
  metadata: input.metadata,
935
478
  actions: input.actions,
@@ -942,6 +485,10 @@ export const platformSetupTools = [
942
485
  'сперва прочитай текущие действия (besales_platform_context_get) и передай ПОЛНЫЙ желаемый набор, ' +
943
486
  'а не дельту, иначе потеряешь не переданные действия.',
944
487
  'Без replace_actions передача actions на существующем триггере отклоняется (защита от случайной потери).',
488
+ 'allow_repeated_calls и run_once_per_contact — РАЗНЫЕ оси, не синонимы: первый про повтор ' +
489
+ 'внутри одного ответа модели, второй запрещает срабатывание навсегда после первого ' +
490
+ 'успешного действия у этого контакта. Для действий, пишущих в чужие системы (заметка в CRM, ' +
491
+ 'создание сделки), нужен именно run_once_per_contact.',
945
492
  ]),
946
493
  }),
947
494
  defineTool({
@@ -1028,37 +575,7 @@ export const platformSetupTools = [
1028
575
  }),
1029
576
  }),
1030
577
  }),
1031
- defineTool({
1032
- name: 'besales_followup_sequence_upsert',
1033
- requiredFields: ['platform_id', 'name', 'type', 'trigger', 'config'],
1034
- schema: z.object({
1035
- platform_id: uuidSchema,
1036
- sequence_id: uuidSchema.optional(),
1037
- name: z.string(),
1038
- description: z.string().nullable().optional(),
1039
- type: followUpSequenceTypeSchema,
1040
- enabled: z.boolean().optional(),
1041
- trigger: followUpTriggerSchema,
1042
- config: anyObjectSchema,
1043
- ...platformSetupConfirmationSchema,
1044
- })
1045
- .superRefine(refineFollowUpConfig),
1046
- createRequest: (input) => ({
1047
- method: input.sequence_id ? 'PATCH' : 'POST',
1048
- path: input.sequence_id
1049
- ? `/platforms/${input.platform_id}/follow-up/sequences/${input.sequence_id}`
1050
- : `/platforms/${input.platform_id}/follow-up/sequences`,
1051
- body: compactObject({
1052
- name: input.name,
1053
- description: input.description,
1054
- type: input.type,
1055
- enabled: input.enabled,
1056
- trigger: input.trigger,
1057
- config: input.config,
1058
- ...setupConfirmation(input),
1059
- }),
1060
- }),
1061
- }),
578
+ ...followUpSetupTools,
1062
579
  defineTool({
1063
580
  name: 'besales_pipeline_settings_update',
1064
581
  requiredFields: ['platform_id'],
@@ -1353,42 +870,66 @@ export const platformSetupTools = [
1353
870
  path: `/agents/${input.agent_id}/prompt/drafts/${input.draft_id}`,
1354
871
  }),
1355
872
  }),
873
+ // Остаток до лимитов линтера ДО сборки черновика. Порядок здесь обязан совпадать с
874
+ // mcp-tools.json (registry.spec проверяет parity), поэтому вставка строго после _patch_get.
875
+ defineTool({
876
+ name: 'besales_prompt_headroom',
877
+ requiredFields: ['agent_id'],
878
+ // .strict() — иначе опечатка `draftId` вместо `draft_id` молча отбрасывается zod'ом,
879
+ // и тулза измеряет закоммиченный промпт вместо черновика.
880
+ schema: z
881
+ .object({
882
+ agent_id: uuidSchema,
883
+ draft_id: uuidSchema.optional(),
884
+ })
885
+ .strict(),
886
+ createRequest: (input) => ({
887
+ method: 'GET',
888
+ // `query` в ApiToolRequest нет — параметр был бы молча выброшен, поэтому клеим в path.
889
+ path: `/agents/${input.agent_id}/prompt/headroom${buildListQuery({ draftId: input.draft_id })}`,
890
+ }),
891
+ enhanceResponse: (response) => withInstructions(response, [
892
+ '`hardHeadroom` — сколько символов ещё можно добавить. Признак перебора — `exceedsHardLimit: true`; ровно на лимите (`hardHeadroom: 0`) commit ещё проходит.',
893
+ 'Если запас меньше объёма планируемой правки — сначала сокращай промпт, а не собирай черновик.',
894
+ '`sections` отсортированы по весу DESC — это кандидаты на сокращение.',
895
+ '`exceedsSoftLimit` — промпт выше рекомендованных 40000: факты стоит вынести в Базу знаний.',
896
+ ]),
897
+ }),
898
+ defineTool({
899
+ name: 'besales_prompt_diff',
900
+ requiredFields: ['agent_id', 'compare_agent_id'],
901
+ schema: z
902
+ .object({
903
+ agent_id: uuidSchema,
904
+ compare_agent_id: uuidSchema,
905
+ max_lines: z.number().int().min(1).max(PROMPT_DIFF_MAX_LINES_LIMIT).optional(),
906
+ })
907
+ .strict(),
908
+ createRequest: (input) => ({
909
+ method: 'POST',
910
+ path: `/agents/${input.agent_id}/prompt/diff`,
911
+ body: { compareAgentId: input.compare_agent_id, maxLines: input.max_lines },
912
+ }),
913
+ enhanceResponse: (response) => withInstructions(response, [
914
+ '`identicalBytes: true` — текущие тексты совпадают побайтово, значит один и тот же набор patch-операций найдёт те же якоря. Это НЕ гарантия одинакового исхода commit: lint резолвит канал каждого агента отдельно (одна и та же markdown-правка проходит в веб-виджете и падает в Telegram), а свой черновик каждый агент коммитит независимо.',
915
+ '`identicalNormalized: true` при `identicalBytes: false` — расходятся только пробелы/переводы строк, правила те же.',
916
+ '`onlyInBase` / `onlyInCompare` — что есть на одной стороне и нет на другой; `count` — превышение кратности.',
917
+ '`truncated: true` — список обрезан по `max_lines`. Обрезка ТЕКСТА строки — отдельный признак `textTruncated`; у таких строк сравнивай `characters`, а не `text`.',
918
+ ]),
919
+ }),
1356
920
  defineTool({
1357
921
  name: 'besales_prompt_patch_apply',
1358
922
  requiredFields: ['agent_id', 'draft_id', 'operations'],
1359
923
  schema: z.object({
1360
924
  agent_id: uuidSchema,
1361
925
  draft_id: uuidSchema,
1362
- operations: z
1363
- .array(z.object({
1364
- id: z.string().optional(),
1365
- type: z.enum([
1366
- 'replace_text',
1367
- 'replace_all_text',
1368
- 'insert_after_text',
1369
- 'insert_before_text',
1370
- 'remove_text',
1371
- 'move_text_after_text',
1372
- ]),
1373
- target_text: z.string().optional(),
1374
- anchor_text: z.string().optional(),
1375
- replacement_text: z.string().optional(),
1376
- match_variants: z.array(z.string()).optional(),
1377
- }))
1378
- .min(1),
926
+ operations: z.array(promptPatchOperationSchema).min(1),
1379
927
  }),
1380
928
  createRequest: (input) => ({
1381
929
  method: 'PATCH',
1382
930
  path: `/agents/${input.agent_id}/prompt/drafts/${input.draft_id}/operations`,
1383
931
  body: {
1384
- operations: input.operations.map((op) => compactObject({
1385
- id: op.id,
1386
- type: op.type,
1387
- targetText: op.target_text,
1388
- anchorText: op.anchor_text,
1389
- replacementText: op.replacement_text,
1390
- matchVariants: op.match_variants,
1391
- })),
932
+ operations: input.operations.map(mapPromptPatchOperationInput),
1392
933
  },
1393
934
  }),
1394
935
  enhanceResponse: (response) => withInstructions(response, [
@@ -1431,6 +972,60 @@ export const platformSetupTools = [
1431
972
  path: `/agents/${input.agent_id}/prompt/drafts/${input.draft_id}`,
1432
973
  }),
1433
974
  }),
975
+ // Пакетная раскатка одной правки по парку агентов. Порядок обязан совпадать с
976
+ // mcp-tools.json — registry.spec сверяет имена позиционно.
977
+ defineTool({
978
+ name: 'besales_prompt_batch_start',
979
+ requiredFields: ['agent_ids', 'operations'],
980
+ schema: z
981
+ .object({
982
+ agent_ids: z.array(uuidSchema).min(1).max(PROMPT_BATCH_MAX_AGENTS),
983
+ operations: z.array(promptPatchOperationSchema).min(1),
984
+ all_or_nothing: z.boolean().optional(),
985
+ idempotency_key: z.string().max(PROMPT_BATCH_IDEMPOTENCY_KEY_MAX_LENGTH).optional(),
986
+ platform_id: uuidSchema.optional(),
987
+ })
988
+ .strict(),
989
+ createRequest: (input) => ({
990
+ method: 'POST',
991
+ path: '/prompt-batch/runs',
992
+ body: compactObject({
993
+ agentIds: input.agent_ids,
994
+ operations: input.operations.map(mapPromptPatchOperationInput),
995
+ allOrNothing: input.all_or_nothing,
996
+ idempotencyKey: input.idempotency_key,
997
+ platformId: input.platform_id,
998
+ }),
999
+ }),
1000
+ enhanceResponse: (response) => withInstructions(response, [
1001
+ 'Прогон фоновый: опрашивай `besales_prompt_batch_status` по `runId`, пока `counts.PENDING` и `counts.PREPARED` не станут нулями.',
1002
+ '`SKIPPED` — не ошибка: смотри `skipReason`, чаще всего это NO_OPERATIONS_APPLIED (у агента другой промпт, якоря не нашлись).',
1003
+ 'Упавших агентов докатывай `besales_prompt_batch_resume` — повторный `_start` создаст ВТОРОЙ прогон и применит операции ещё раз.',
1004
+ 'Перед раскаткой на парк проверь запас длины у самого тяжёлого агента через `besales_prompt_headroom`.',
1005
+ ]),
1006
+ }),
1007
+ defineTool({
1008
+ name: 'besales_prompt_batch_status',
1009
+ requiredFields: ['run_id'],
1010
+ schema: z.object({ run_id: uuidSchema }).strict(),
1011
+ createRequest: (input) => ({
1012
+ method: 'GET',
1013
+ path: `/prompt-batch/runs/${input.run_id}`,
1014
+ }),
1015
+ }),
1016
+ defineTool({
1017
+ name: 'besales_prompt_batch_resume',
1018
+ requiredFields: ['run_id'],
1019
+ schema: z.object({ run_id: uuidSchema }).strict(),
1020
+ createRequest: (input) => ({
1021
+ method: 'POST',
1022
+ path: `/prompt-batch/runs/${input.run_id}/resume`,
1023
+ }),
1024
+ enhanceResponse: (response) => withInstructions(response, [
1025
+ 'Докатываются только FAILED-агенты. Уже закоммиченные не трогаются — повторное применение операций удвоило бы вставки.',
1026
+ '400 «Run is still in progress» означает, что прогон жив: дождись, а не повторяй resume.',
1027
+ ]),
1028
+ }),
1434
1029
  // S2.6 extra — besales_prompt_lint (после patches, чтобы matched catalog order)
1435
1030
  defineTool({
1436
1031
  name: 'besales_prompt_lint',