@besales/mcp 0.48.0 → 1.0.0-beta.4

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 (604) hide show
  1. package/README.md +117 -163
  2. package/README.onprem.ru.md +35 -0
  3. package/bin/besales-mcp-http.js +9 -0
  4. package/bin/besales-onprem.mjs +19 -0
  5. package/dist/auth/connection-store.d.ts +3 -1
  6. package/dist/auth/connection-store.js +9 -3
  7. package/dist/auth/connection-store.js.map +1 -1
  8. package/dist/auth/oauth-client.d.ts +20 -27
  9. package/dist/auth/oauth-client.js +31 -23
  10. package/dist/auth/oauth-client.js.map +1 -1
  11. package/dist/auth/session-workspace.d.ts +1 -0
  12. package/dist/auth/session-workspace.js +10 -1
  13. package/dist/auth/session-workspace.js.map +1 -1
  14. package/dist/auth/token-storage.js +4 -1
  15. package/dist/auth/token-storage.js.map +1 -1
  16. package/dist/cli.d.ts +1 -0
  17. package/dist/cli.js +80 -19
  18. package/dist/cli.js.map +1 -1
  19. package/dist/constants/crm-catalog.d.ts +2 -0
  20. package/dist/constants/crm-catalog.js +3 -0
  21. package/dist/constants/crm-catalog.js.map +1 -0
  22. package/dist/http/api-client.d.ts +54 -7
  23. package/dist/http/api-client.js +132 -66
  24. package/dist/http/api-client.js.map +1 -1
  25. package/dist/http/feedback-document-error.d.ts +26 -0
  26. package/dist/http/feedback-document-error.js +22 -0
  27. package/dist/http/feedback-document-error.js.map +1 -0
  28. package/dist/http-server.d.ts +15 -0
  29. package/dist/http-server.js +378 -0
  30. package/dist/http-server.js.map +1 -0
  31. package/dist/instructions/server-instructions.d.ts +1 -1
  32. package/dist/instructions/server-instructions.js +6 -3
  33. package/dist/instructions/server-instructions.js.map +1 -1
  34. package/dist/network/constants.d.ts +25 -0
  35. package/dist/network/constants.js +26 -0
  36. package/dist/network/constants.js.map +1 -0
  37. package/dist/network/failed-verifications.d.ts +10 -0
  38. package/dist/network/failed-verifications.js +37 -0
  39. package/dist/network/failed-verifications.js.map +1 -0
  40. package/dist/network/http-test-fixture.d.ts +3 -0
  41. package/dist/network/http-test-fixture.js +86 -0
  42. package/dist/network/http-test-fixture.js.map +1 -0
  43. package/dist/network/known-keys.d.ts +10 -0
  44. package/dist/network/known-keys.js +38 -0
  45. package/dist/network/known-keys.js.map +1 -0
  46. package/dist/network/memory-credentials.d.ts +11 -0
  47. package/dist/network/memory-credentials.js +24 -0
  48. package/dist/network/memory-credentials.js.map +1 -0
  49. package/dist/network/rate-limit.d.ts +23 -0
  50. package/dist/network/rate-limit.js +62 -0
  51. package/dist/network/rate-limit.js.map +1 -0
  52. package/dist/network/rejected-keys.d.ts +9 -0
  53. package/dist/network/rejected-keys.js +39 -0
  54. package/dist/network/rejected-keys.js.map +1 -0
  55. package/dist/network/request-work.d.ts +8 -0
  56. package/dist/network/request-work.js +20 -0
  57. package/dist/network/request-work.js.map +1 -0
  58. package/dist/network/successful-keys.d.ts +17 -0
  59. package/dist/network/successful-keys.js +47 -0
  60. package/dist/network/successful-keys.js.map +1 -0
  61. package/dist/resources/concepts/external-execution.md +3 -1
  62. package/dist/resources/concepts/feedback-sheets.md +21 -12
  63. package/dist/resources/concepts/icp.md +8 -1
  64. package/dist/resources/concepts/sandbox.md +15 -6
  65. package/dist/resources/concepts/triggers.md +43 -0
  66. package/dist/resources/concepts/workbook-classification.md +104 -50
  67. package/dist/resources/docs/agent-behavior.md +51 -382
  68. package/dist/resources/docs/campaigns.md +9 -16
  69. package/dist/resources/docs/crm-integration.md +39 -523
  70. package/dist/resources/docs/files-and-uploads.md +23 -286
  71. package/dist/resources/docs/follow-up.md +47 -12
  72. package/dist/resources/docs/knowledge-base.md +52 -521
  73. package/dist/resources/docs/pipeline-builder.md +26 -216
  74. package/dist/resources/docs/pipeline-settings-deep.md +27 -220
  75. package/dist/resources/docs/platforms.md +149 -519
  76. package/dist/resources/docs/prompt-anatomy.md +39 -285
  77. package/dist/resources/docs/routing.md +8 -16
  78. package/dist/resources/docs/trigger-actions.md +40 -24
  79. package/dist/resources/docs/variables.md +8 -12
  80. package/dist/resources/operator-edit.contract.d.ts +7 -0
  81. package/dist/resources/operator-edit.contract.js +8 -0
  82. package/dist/resources/operator-edit.contract.js.map +1 -0
  83. package/dist/resources/registry.js +20 -4
  84. package/dist/resources/registry.js.map +1 -1
  85. package/dist/resources/workflows/audit-dialogues.md +23 -12
  86. package/dist/resources/workflows/compare-models.md +6 -6
  87. package/dist/resources/workflows/connect-crm-from-scratch.md +15 -12
  88. package/dist/resources/workflows/connect-datasource-from-scratch.md +5 -4
  89. package/dist/resources/workflows/extract-from-document.md +8 -6
  90. package/dist/resources/workflows/operator-edit.md +15 -0
  91. package/dist/resources/workflows/platform-setup-from-scratch.md +59 -70
  92. package/dist/resources/workflows/production-readiness-check.md +25 -15
  93. package/dist/schemas/mcp-tools.json +12389 -6657
  94. package/dist/schemas/voice-actions-api.contract.json +248 -0
  95. package/dist/shared/constants/broadcast-reason.constants.d.ts +2 -0
  96. package/dist/shared/constants/broadcast-reason.constants.js +47 -0
  97. package/dist/shared/constants/broadcast-reason.constants.js.map +1 -0
  98. package/dist/shared/constants/crm-stage-pool.constants.d.ts +2 -0
  99. package/dist/shared/constants/crm-stage-pool.constants.js +3 -0
  100. package/dist/shared/constants/crm-stage-pool.constants.js.map +1 -0
  101. package/dist/shared/constants/followup-note.constants.d.ts +3 -0
  102. package/dist/shared/constants/followup-note.constants.js +4 -0
  103. package/dist/shared/constants/followup-note.constants.js.map +1 -0
  104. package/dist/shared/constants/followup-session.constants.d.ts +2 -0
  105. package/dist/shared/constants/followup-session.constants.js +3 -0
  106. package/dist/shared/constants/followup-session.constants.js.map +1 -0
  107. package/dist/shared/constants/funnel-analytics.constants.d.ts +2 -0
  108. package/dist/shared/constants/funnel-analytics.constants.js +3 -0
  109. package/dist/shared/constants/funnel-analytics.constants.js.map +1 -0
  110. package/dist/shared/constants/message-filter.constants.d.ts +7 -0
  111. package/dist/shared/constants/message-filter.constants.js +8 -0
  112. package/dist/shared/constants/message-filter.constants.js.map +1 -0
  113. package/dist/shared/utils/trigger-repeat-mapping.d.ts +72 -0
  114. package/dist/shared/utils/trigger-repeat-mapping.js +53 -0
  115. package/dist/shared/utils/trigger-repeat-mapping.js.map +1 -0
  116. package/dist/skills/besales-dialogue-audit/SKILL.md +31 -20
  117. package/dist/tasks/task-state-store.js +5 -0
  118. package/dist/tasks/task-state-store.js.map +1 -1
  119. package/dist/tools/annotations.js +4 -3
  120. package/dist/tools/annotations.js.map +1 -1
  121. package/dist/tools/definitions/agent-design.d.ts +81 -24
  122. package/dist/tools/definitions/agent-design.js +246 -90
  123. package/dist/tools/definitions/agent-design.js.map +1 -1
  124. package/dist/tools/definitions/agent-model-notice.d.ts +5 -0
  125. package/dist/tools/definitions/agent-model-notice.js +31 -0
  126. package/dist/tools/definitions/agent-model-notice.js.map +1 -0
  127. package/dist/tools/definitions/agent-models.d.ts +5 -0
  128. package/dist/tools/definitions/agent-models.js +26 -0
  129. package/dist/tools/definitions/agent-models.js.map +1 -0
  130. package/dist/tools/definitions/agent-settings.d.ts +85 -0
  131. package/dist/tools/definitions/agent-settings.js +187 -0
  132. package/dist/tools/definitions/agent-settings.js.map +1 -0
  133. package/dist/tools/definitions/ai-costs.d.ts +12 -0
  134. package/dist/tools/definitions/ai-costs.js +142 -0
  135. package/dist/tools/definitions/ai-costs.js.map +1 -0
  136. package/dist/tools/definitions/api-tool-request.d.ts +15 -0
  137. package/dist/tools/definitions/api-tool-request.js +2 -0
  138. package/dist/tools/definitions/api-tool-request.js.map +1 -0
  139. package/dist/tools/definitions/behavior-shipped-form.d.ts +182 -0
  140. package/dist/tools/definitions/behavior-shipped-form.js +185 -0
  141. package/dist/tools/definitions/behavior-shipped-form.js.map +1 -0
  142. package/dist/tools/definitions/broadcast-alerts.d.ts +6 -0
  143. package/dist/tools/definitions/broadcast-alerts.js +72 -0
  144. package/dist/tools/definitions/broadcast-alerts.js.map +1 -0
  145. package/dist/tools/definitions/broadcast-analytics.d.ts +102 -18
  146. package/dist/tools/definitions/broadcast-analytics.js +732 -431
  147. package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
  148. package/dist/tools/definitions/broadcast-scheduler.d.ts +10 -13
  149. package/dist/tools/definitions/broadcast-scheduler.js +196 -154
  150. package/dist/tools/definitions/broadcast-scheduler.js.map +1 -1
  151. package/dist/tools/definitions/call-operator-task.schema.d.ts +11 -0
  152. package/dist/tools/definitions/call-operator-task.schema.js +30 -0
  153. package/dist/tools/definitions/call-operator-task.schema.js.map +1 -0
  154. package/dist/tools/definitions/contacts.d.ts +3 -0
  155. package/dist/tools/definitions/contacts.js +57 -31
  156. package/dist/tools/definitions/contacts.js.map +1 -1
  157. package/dist/tools/definitions/crm-platform.d.ts +29 -39
  158. package/dist/tools/definitions/crm-platform.js +249 -352
  159. package/dist/tools/definitions/crm-platform.js.map +1 -1
  160. package/dist/tools/definitions/crm-stage-pool.messages.d.ts +39 -0
  161. package/dist/tools/definitions/crm-stage-pool.messages.js +43 -0
  162. package/dist/tools/definitions/crm-stage-pool.messages.js.map +1 -0
  163. package/dist/tools/definitions/crm-stage-pool.schema.d.ts +64 -0
  164. package/dist/tools/definitions/crm-stage-pool.schema.js +109 -0
  165. package/dist/tools/definitions/crm-stage-pool.schema.js.map +1 -0
  166. package/dist/tools/definitions/custom-reports.d.ts +12 -0
  167. package/dist/tools/definitions/custom-reports.js +68 -0
  168. package/dist/tools/definitions/custom-reports.js.map +1 -0
  169. package/dist/tools/definitions/datasource.d.ts +2 -1
  170. package/dist/tools/definitions/datasource.js +28 -13
  171. package/dist/tools/definitions/datasource.js.map +1 -1
  172. package/dist/tools/definitions/dialog-review-findings-response.d.ts +14 -0
  173. package/dist/tools/definitions/dialog-review-findings-response.js +23 -0
  174. package/dist/tools/definitions/dialog-review-findings-response.js.map +1 -0
  175. package/dist/tools/definitions/dialog-review-safe-response.d.ts +5 -0
  176. package/dist/tools/definitions/dialog-review-safe-response.js +152 -0
  177. package/dist/tools/definitions/dialog-review-safe-response.js.map +1 -0
  178. package/dist/tools/definitions/dialog-review.d.ts +49 -0
  179. package/dist/tools/definitions/dialog-review.js +383 -0
  180. package/dist/tools/definitions/dialog-review.js.map +1 -0
  181. package/dist/tools/definitions/dialogue-analysis.d.ts +46 -0
  182. package/dist/tools/definitions/dialogue-analysis.js +170 -0
  183. package/dist/tools/definitions/dialogue-analysis.js.map +1 -0
  184. package/dist/tools/definitions/dialogue-audit-agent-scope.d.ts +7 -0
  185. package/dist/tools/definitions/dialogue-audit-agent-scope.js +41 -0
  186. package/dist/tools/definitions/dialogue-audit-agent-scope.js.map +1 -0
  187. package/dist/tools/definitions/dialogue-audit-alias.d.ts +67 -0
  188. package/dist/tools/definitions/dialogue-audit-alias.js +559 -0
  189. package/dist/tools/definitions/dialogue-audit-alias.js.map +1 -0
  190. package/dist/tools/definitions/dialogue-export.d.ts +7 -1
  191. package/dist/tools/definitions/dialogue-export.js +89 -32
  192. package/dist/tools/definitions/dialogue-export.js.map +1 -1
  193. package/dist/tools/definitions/dialogue-followup-state.d.ts +9 -0
  194. package/dist/tools/definitions/dialogue-followup-state.js +69 -0
  195. package/dist/tools/definitions/dialogue-followup-state.js.map +1 -0
  196. package/dist/tools/definitions/dialogue-reprocess.d.ts +5 -0
  197. package/dist/tools/definitions/dialogue-reprocess.js +107 -0
  198. package/dist/tools/definitions/dialogue-reprocess.js.map +1 -0
  199. package/dist/tools/definitions/edit-version-transport.test-fixtures.d.ts +13 -0
  200. package/dist/tools/definitions/edit-version-transport.test-fixtures.js +292 -0
  201. package/dist/tools/definitions/edit-version-transport.test-fixtures.js.map +1 -0
  202. package/dist/tools/definitions/edit-version.d.ts +5 -0
  203. package/dist/tools/definitions/edit-version.js +20 -0
  204. package/dist/tools/definitions/edit-version.js.map +1 -0
  205. package/dist/tools/definitions/external-api-channels.js +17 -15
  206. package/dist/tools/definitions/external-api-channels.js.map +1 -1
  207. package/dist/tools/definitions/feedback-document-access.d.ts +16 -0
  208. package/dist/tools/definitions/feedback-document-access.js +63 -0
  209. package/dist/tools/definitions/feedback-document-access.js.map +1 -0
  210. package/dist/tools/definitions/fixtures/shipped-setup-status.d.ts +3 -0
  211. package/dist/tools/definitions/fixtures/shipped-setup-status.js +9 -0
  212. package/dist/tools/definitions/fixtures/shipped-setup-status.js.map +1 -0
  213. package/dist/tools/definitions/follow-up-config.d.ts +79 -0
  214. package/dist/tools/definitions/follow-up-config.js +258 -0
  215. package/dist/tools/definitions/follow-up-config.js.map +1 -0
  216. package/dist/tools/definitions/follow-up-funnel-test.d.ts +4 -3
  217. package/dist/tools/definitions/follow-up-funnel-test.js +65 -52
  218. package/dist/tools/definitions/follow-up-funnel-test.js.map +1 -1
  219. package/dist/tools/definitions/follow-up-promotion-instructions.d.ts +2 -0
  220. package/dist/tools/definitions/follow-up-promotion-instructions.js +7 -0
  221. package/dist/tools/definitions/follow-up-promotion-instructions.js.map +1 -0
  222. package/dist/tools/definitions/follow-up-setup.d.ts +4 -28
  223. package/dist/tools/definitions/follow-up-setup.js +20 -87
  224. package/dist/tools/definitions/follow-up-setup.js.map +1 -1
  225. package/dist/tools/definitions/follow-up.d.ts +20 -122
  226. package/dist/tools/definitions/follow-up.js +200 -357
  227. package/dist/tools/definitions/follow-up.js.map +1 -1
  228. package/dist/tools/definitions/followup-getcourse-compat.d.ts +29 -0
  229. package/dist/tools/definitions/followup-getcourse-compat.js +62 -0
  230. package/dist/tools/definitions/followup-getcourse-compat.js.map +1 -0
  231. package/dist/tools/definitions/followup-sequence-config.schema.d.ts +10 -0
  232. package/dist/tools/definitions/followup-sequence-config.schema.js +14 -0
  233. package/dist/tools/definitions/followup-sequence-config.schema.js.map +1 -0
  234. package/dist/tools/definitions/funnel-analytics.d.ts +18 -0
  235. package/dist/tools/definitions/funnel-analytics.js +150 -0
  236. package/dist/tools/definitions/funnel-analytics.js.map +1 -0
  237. package/dist/tools/definitions/getcourse-anchor-refresh.d.ts +5 -9
  238. package/dist/tools/definitions/getcourse-anchor-refresh.js +26 -31
  239. package/dist/tools/definitions/getcourse-anchor-refresh.js.map +1 -1
  240. package/dist/tools/definitions/getcourse-cold-outbound-settings.d.ts +46 -0
  241. package/dist/tools/definitions/getcourse-cold-outbound-settings.js +37 -0
  242. package/dist/tools/definitions/getcourse-cold-outbound-settings.js.map +1 -0
  243. package/dist/tools/definitions/getcourse-cold-welcome.d.ts +14 -29
  244. package/dist/tools/definitions/getcourse-cold-welcome.js +190 -253
  245. package/dist/tools/definitions/getcourse-cold-welcome.js.map +1 -1
  246. package/dist/tools/definitions/getcourse-wave-welcome.messages.d.ts +10 -0
  247. package/dist/tools/definitions/getcourse-wave-welcome.messages.js +17 -0
  248. package/dist/tools/definitions/getcourse-wave-welcome.messages.js.map +1 -0
  249. package/dist/tools/definitions/help.js.map +1 -1
  250. package/dist/tools/definitions/icp-import-source.d.ts +75 -0
  251. package/dist/tools/definitions/icp-import-source.js +88 -0
  252. package/dist/tools/definitions/icp-import-source.js.map +1 -0
  253. package/dist/tools/definitions/inbound-analytics.d.ts +13 -0
  254. package/dist/tools/definitions/inbound-analytics.js +55 -0
  255. package/dist/tools/definitions/inbound-analytics.js.map +1 -0
  256. package/dist/tools/definitions/instagram-comment.templates.d.ts +19 -0
  257. package/dist/tools/definitions/instagram-comment.templates.js +34 -0
  258. package/dist/tools/definitions/instagram-comment.templates.js.map +1 -0
  259. package/dist/tools/definitions/instagram-settings-compat.d.ts +33 -0
  260. package/dist/tools/definitions/instagram-settings-compat.js +7 -0
  261. package/dist/tools/definitions/instagram-settings-compat.js.map +1 -0
  262. package/dist/tools/definitions/knowledge-chunking.d.ts +42 -0
  263. package/dist/tools/definitions/knowledge-chunking.js +83 -0
  264. package/dist/tools/definitions/knowledge-chunking.js.map +1 -0
  265. package/dist/tools/definitions/knowledge-space-search-fields.d.ts +12 -0
  266. package/dist/tools/definitions/knowledge-space-search-fields.js +30 -0
  267. package/dist/tools/definitions/knowledge-space-search-fields.js.map +1 -0
  268. package/dist/tools/definitions/knowledge.d.ts +24 -14
  269. package/dist/tools/definitions/knowledge.js +342 -233
  270. package/dist/tools/definitions/knowledge.js.map +1 -1
  271. package/dist/tools/definitions/legacy-trigger-repeat.d.ts +17 -0
  272. package/dist/tools/definitions/legacy-trigger-repeat.js +53 -0
  273. package/dist/tools/definitions/legacy-trigger-repeat.js.map +1 -0
  274. package/dist/tools/definitions/link-tracking.d.ts +8 -0
  275. package/dist/tools/definitions/link-tracking.js +141 -38
  276. package/dist/tools/definitions/link-tracking.js.map +1 -1
  277. package/dist/tools/definitions/mailing-variant-content.d.ts +75 -0
  278. package/dist/tools/definitions/mailing-variant-content.js +73 -0
  279. package/dist/tools/definitions/mailing-variant-content.js.map +1 -0
  280. package/dist/tools/definitions/model-comparison.d.ts +2 -1
  281. package/dist/tools/definitions/model-comparison.js +4 -5
  282. package/dist/tools/definitions/model-comparison.js.map +1 -1
  283. package/dist/tools/definitions/money-currency.d.ts +5 -0
  284. package/dist/tools/definitions/money-currency.js +10 -0
  285. package/dist/tools/definitions/money-currency.js.map +1 -0
  286. package/dist/tools/definitions/operator-workflow.d.ts +5 -5
  287. package/dist/tools/definitions/operator-workflow.js +80 -27
  288. package/dist/tools/definitions/operator-workflow.js.map +1 -1
  289. package/dist/tools/definitions/platform-funnel-analytics.d.ts +42 -0
  290. package/dist/tools/definitions/platform-funnel-analytics.js +72 -0
  291. package/dist/tools/definitions/platform-funnel-analytics.js.map +1 -0
  292. package/dist/tools/definitions/platform-getcourse-wave-field-bindings.d.ts +42 -0
  293. package/dist/tools/definitions/platform-getcourse-wave-field-bindings.js +51 -0
  294. package/dist/tools/definitions/platform-getcourse-wave-field-bindings.js.map +1 -0
  295. package/dist/tools/definitions/platform-settings-limits.d.ts +5 -0
  296. package/dist/tools/definitions/platform-settings-limits.js +6 -0
  297. package/dist/tools/definitions/platform-settings-limits.js.map +1 -0
  298. package/dist/tools/definitions/platform-settings-shipped-form.d.ts +37 -0
  299. package/dist/tools/definitions/platform-settings-shipped-form.js +310 -0
  300. package/dist/tools/definitions/platform-settings-shipped-form.js.map +1 -0
  301. package/dist/tools/definitions/platform-settings-update.d.ts +300 -0
  302. package/dist/tools/definitions/platform-settings-update.js +134 -0
  303. package/dist/tools/definitions/platform-settings-update.js.map +1 -0
  304. package/dist/tools/definitions/{getcourse-wave.d.ts → platform-setup-wave.d.ts} +75 -6
  305. package/dist/tools/definitions/platform-setup-wave.js +1079 -0
  306. package/dist/tools/definitions/platform-setup-wave.js.map +1 -0
  307. package/dist/tools/definitions/platform-setup.d.ts +697 -224
  308. package/dist/tools/definitions/platform-setup.js +313 -752
  309. package/dist/tools/definitions/platform-setup.js.map +1 -1
  310. package/dist/tools/definitions/platform-widget-page-rules.schema.d.ts +10 -0
  311. package/dist/tools/definitions/platform-widget-page-rules.schema.js +7 -0
  312. package/dist/tools/definitions/platform-widget-page-rules.schema.js.map +1 -0
  313. package/dist/tools/definitions/prompt-authoring.constants.d.ts +2 -0
  314. package/dist/tools/definitions/prompt-authoring.constants.js +3 -0
  315. package/dist/tools/definitions/prompt-authoring.constants.js.map +1 -0
  316. package/dist/tools/definitions/prompt-batch-controls.d.ts +5 -0
  317. package/dist/tools/definitions/prompt-batch-controls.js +40 -0
  318. package/dist/tools/definitions/prompt-batch-controls.js.map +1 -0
  319. package/dist/tools/definitions/prompt-draft-authoring.d.ts +5 -0
  320. package/dist/tools/definitions/prompt-draft-authoring.js +24 -0
  321. package/dist/tools/definitions/prompt-draft-authoring.js.map +1 -0
  322. package/dist/tools/definitions/prompt-text-projection.d.ts +11 -0
  323. package/dist/tools/definitions/prompt-text-projection.js +62 -0
  324. package/dist/tools/definitions/prompt-text-projection.js.map +1 -0
  325. package/dist/tools/definitions/reviewer-paid.d.ts +19 -0
  326. package/dist/tools/definitions/reviewer-paid.js +115 -0
  327. package/dist/tools/definitions/reviewer-paid.js.map +1 -0
  328. package/dist/tools/definitions/routing-condition-projection.d.ts +31 -0
  329. package/dist/tools/definitions/routing-condition-projection.js +97 -0
  330. package/dist/tools/definitions/routing-condition-projection.js.map +1 -0
  331. package/dist/tools/definitions/schedule-selection-hints.d.ts +3 -0
  332. package/dist/tools/definitions/schedule-selection-hints.js +42 -0
  333. package/dist/tools/definitions/schedule-selection-hints.js.map +1 -0
  334. package/dist/tools/definitions/{scheduled-tasks.d.ts → scheduled-task-alias.d.ts} +12 -10
  335. package/dist/tools/definitions/scheduled-task-alias.js +207 -0
  336. package/dist/tools/definitions/scheduled-task-alias.js.map +1 -0
  337. package/dist/tools/definitions/session.js +3 -3
  338. package/dist/tools/definitions/session.js.map +1 -1
  339. package/dist/tools/definitions/setup-init-operation.schema.d.ts +2 -0
  340. package/dist/tools/definitions/setup-init-operation.schema.js +6 -0
  341. package/dist/tools/definitions/setup-init-operation.schema.js.map +1 -0
  342. package/dist/tools/definitions/shared-input.helpers.d.ts +51 -0
  343. package/dist/tools/definitions/shared-input.helpers.js +323 -0
  344. package/dist/tools/definitions/shared-input.helpers.js.map +1 -0
  345. package/dist/tools/definitions/shared.d.ts +401 -307
  346. package/dist/tools/definitions/shared.js +430 -652
  347. package/dist/tools/definitions/shared.js.map +1 -1
  348. package/dist/tools/definitions/shipped-output-names.d.ts +19 -0
  349. package/dist/tools/definitions/shipped-output-names.js +123 -0
  350. package/dist/tools/definitions/shipped-output-names.js.map +1 -0
  351. package/dist/tools/definitions/task-state.d.ts +4 -4
  352. package/dist/tools/definitions/task-state.js +2 -1
  353. package/dist/tools/definitions/task-state.js.map +1 -1
  354. package/dist/tools/definitions/telegram-personal-capacity.d.ts +4 -0
  355. package/dist/tools/definitions/telegram-personal-capacity.js +18 -0
  356. package/dist/tools/definitions/telegram-personal-capacity.js.map +1 -0
  357. package/dist/tools/definitions/telegram-personal-management.d.ts +34 -0
  358. package/dist/tools/definitions/telegram-personal-management.js +139 -0
  359. package/dist/tools/definitions/telegram-personal-management.js.map +1 -0
  360. package/dist/tools/definitions/telegram-personal-restrictions.d.ts +19 -0
  361. package/dist/tools/definitions/telegram-personal-restrictions.js +109 -0
  362. package/dist/tools/definitions/telegram-personal-restrictions.js.map +1 -0
  363. package/dist/tools/definitions/telegram-pool-policy.d.ts +8 -0
  364. package/dist/tools/definitions/telegram-pool-policy.js +56 -0
  365. package/dist/tools/definitions/telegram-pool-policy.js.map +1 -0
  366. package/dist/tools/definitions/touch-history-alias.d.ts +4 -0
  367. package/dist/tools/definitions/touch-history-alias.js +99 -0
  368. package/dist/tools/definitions/touch-history-alias.js.map +1 -0
  369. package/dist/tools/definitions/trigger-action-item.schema.d.ts +254 -0
  370. package/dist/tools/definitions/trigger-action-item.schema.js +121 -0
  371. package/dist/tools/definitions/trigger-action-item.schema.js.map +1 -0
  372. package/dist/tools/definitions/trigger-argument.schema.d.ts +14 -0
  373. package/dist/tools/definitions/trigger-argument.schema.js +23 -0
  374. package/dist/tools/definitions/trigger-argument.schema.js.map +1 -0
  375. package/dist/tools/definitions/trigger-claim-alias.d.ts +10 -0
  376. package/dist/tools/definitions/trigger-claim-alias.js +90 -0
  377. package/dist/tools/definitions/trigger-claim-alias.js.map +1 -0
  378. package/dist/tools/definitions/trigger-echo-projection.d.ts +1 -0
  379. package/dist/tools/definitions/trigger-echo-projection.js +41 -0
  380. package/dist/tools/definitions/trigger-echo-projection.js.map +1 -0
  381. package/dist/tools/definitions/trigger-upsert.tool.d.ts +41 -0
  382. package/dist/tools/definitions/trigger-upsert.tool.js +159 -0
  383. package/dist/tools/definitions/trigger-upsert.tool.js.map +1 -0
  384. package/dist/tools/definitions/v15-port-descriptions.d.ts +11 -0
  385. package/dist/tools/definitions/v15-port-descriptions.js +30 -0
  386. package/dist/tools/definitions/v15-port-descriptions.js.map +1 -0
  387. package/dist/tools/definitions/variables.d.ts +44 -0
  388. package/dist/tools/definitions/variables.js +310 -0
  389. package/dist/tools/definitions/variables.js.map +1 -0
  390. package/dist/tools/definitions/voice-action-input.d.ts +9 -0
  391. package/dist/tools/definitions/voice-action-input.js +141 -0
  392. package/dist/tools/definitions/voice-action-input.js.map +1 -0
  393. package/dist/tools/definitions/widget-customization-compat.d.ts +51 -0
  394. package/dist/tools/definitions/widget-customization-compat.js +23 -0
  395. package/dist/tools/definitions/widget-customization-compat.js.map +1 -0
  396. package/dist/tools/definitions.d.ts +1134 -617
  397. package/dist/tools/definitions.js +63 -27
  398. package/dist/tools/definitions.js.map +1 -1
  399. package/dist/tools/description-localization.js +3 -2
  400. package/dist/tools/description-localization.js.map +1 -1
  401. package/dist/tools/follow-up-request.d.ts +19 -0
  402. package/dist/tools/follow-up-request.js +81 -0
  403. package/dist/tools/follow-up-request.js.map +1 -0
  404. package/dist/tools/followup-revision.d.ts +4 -0
  405. package/dist/tools/followup-revision.js +29 -0
  406. package/dist/tools/followup-revision.js.map +1 -0
  407. package/dist/tools/incomplete-list.js +4 -0
  408. package/dist/tools/incomplete-list.js.map +1 -1
  409. package/dist/tools/inventory.js +47 -11
  410. package/dist/tools/inventory.js.map +1 -1
  411. package/dist/tools/operation-recovery.d.ts +4 -0
  412. package/dist/tools/operation-recovery.js +33 -0
  413. package/dist/tools/operation-recovery.js.map +1 -0
  414. package/dist/tools/registry-mapping-primary.cases.d.ts +2 -0
  415. package/dist/tools/registry-mapping-primary.cases.js +765 -0
  416. package/dist/tools/registry-mapping-primary.cases.js.map +1 -0
  417. package/dist/tools/registry-mapping-secondary.cases.d.ts +2 -0
  418. package/dist/tools/registry-mapping-secondary.cases.js +626 -0
  419. package/dist/tools/registry-mapping-secondary.cases.js.map +1 -0
  420. package/dist/tools/registry-mapping.cases.d.ts +8 -0
  421. package/dist/tools/registry-mapping.cases.js +170 -0
  422. package/dist/tools/registry-mapping.cases.js.map +1 -0
  423. package/dist/tools/registry-test-fixtures.d.ts +50 -0
  424. package/dist/tools/registry-test-fixtures.js +120 -0
  425. package/dist/tools/registry-test-fixtures.js.map +1 -0
  426. package/dist/tools/registry.d.ts +2 -0
  427. package/dist/tools/registry.js +84 -52
  428. package/dist/tools/registry.js.map +1 -1
  429. package/dist/tools/result.d.ts +1 -0
  430. package/dist/tools/result.js +90 -22
  431. package/dist/tools/result.js.map +1 -1
  432. package/dist/tools/toolsets.d.ts +16 -0
  433. package/dist/tools/toolsets.js +25 -1
  434. package/dist/tools/toolsets.js.map +1 -1
  435. package/dist/types/api-contract.gen.d.ts +131892 -22453
  436. package/dist/utils/credential-signatures.d.ts +3 -0
  437. package/dist/utils/credential-signatures.js +10 -0
  438. package/dist/utils/credential-signatures.js.map +1 -0
  439. package/dist/utils/data-source-column-name.d.ts +10 -0
  440. package/dist/utils/data-source-column-name.js +36 -0
  441. package/dist/utils/data-source-column-name.js.map +1 -0
  442. package/dist/utils/logger.d.ts +1 -0
  443. package/dist/utils/logger.js +32 -8
  444. package/dist/utils/logger.js.map +1 -1
  445. package/dist/utils/secret-read-boundary.d.ts +2 -0
  446. package/dist/utils/secret-read-boundary.js +88 -0
  447. package/dist/utils/secret-read-boundary.js.map +1 -0
  448. package/dist/utils/setup-secrets.d.ts +4 -0
  449. package/dist/utils/setup-secrets.js +40 -0
  450. package/dist/utils/setup-secrets.js.map +1 -0
  451. package/docs/agent-models.md +9 -0
  452. package/docs/agent-usage-guide.full.ru.md +1 -4
  453. package/docs/analytics-contract.md +67 -0
  454. package/docs/async-operation-lifecycle.md +10 -3
  455. package/docs/connect.ru.md +144 -4
  456. package/docs/contact-company-profile.md +17 -0
  457. package/docs/contact-recommendations.md +7 -0
  458. package/docs/crm-stage-pool.md +73 -0
  459. package/docs/customer-data.md +61 -0
  460. package/docs/dialogue-reprocess.md +37 -0
  461. package/docs/edit-versions.md +36 -0
  462. package/docs/failure-semantics.md +17 -9
  463. package/docs/help-navigation.md +15 -42
  464. package/docs/host-compatibility-matrix.md +72 -93
  465. package/docs/host-setup.md +33 -35
  466. package/docs/invariants/public-api-and-contracts.md +16 -0
  467. package/docs/legacy-action-settings.md +21 -0
  468. package/docs/main-transfer-dependencies.md +38 -0
  469. package/docs/mcp2-parity.md +50 -0
  470. package/docs/modules/analytics.md +79 -0
  471. package/docs/modules/besales-mcp.md +144 -0
  472. package/docs/modules/dialog-review-report.md +38 -0
  473. package/docs/modules/dialogue-followup-adapters.md +39 -0
  474. package/docs/modules/message-reactions.md +17 -0
  475. package/docs/modules/network-mode.md +284 -0
  476. package/docs/modules/prompt-batch-and-dialogue-controls.md +69 -0
  477. package/docs/pii-policy.md +21 -16
  478. package/docs/response-size-budget.md +197 -7
  479. package/docs/secret-read-boundary.md +102 -0
  480. package/docs/task-state.md +7 -2
  481. package/docs/telegram-personal-management.md +50 -0
  482. package/docs/telegram-personal-restrictions.md +87 -0
  483. package/docs/telegram-pool-policy.md +19 -0
  484. package/docs/tool-effects.md +15 -9
  485. package/docs/tool-surface.md +205 -4
  486. package/docs/voice-actions-and-inbox.md +95 -0
  487. package/docs/wazzup-instagram-comments.md +26 -0
  488. package/package.json +17 -10
  489. package/scripts/api-source-loader.spec.mjs +40 -0
  490. package/scripts/audit-bridge-controls.mjs +129 -0
  491. package/scripts/audit-revert-mutants.mjs +107 -0
  492. package/scripts/bridge-api-input.spec.mjs +98 -0
  493. package/scripts/bridge-controls-mutants.mjs +130 -0
  494. package/scripts/brx3-shipped-refusals.spec.mjs +149 -0
  495. package/scripts/brx4-ledger-payload.spec.mjs +127 -0
  496. package/scripts/build-onprem.mjs +108 -0
  497. package/scripts/connect-clients.sh +141 -0
  498. package/scripts/connect-scopes.mjs +166 -0
  499. package/scripts/connect-scopes.spec.mjs +83 -0
  500. package/scripts/emit-personal-link-carrier.mjs +31 -0
  501. package/scripts/emit-setup-init-schemas.mjs +24 -0
  502. package/scripts/emit-tool-routes.mjs +878 -0
  503. package/scripts/emit-tool-routes.spec.mjs +300 -0
  504. package/scripts/followup-registry.spec.mjs +107 -0
  505. package/scripts/generate-crm-rotation-contract.mjs +98 -0
  506. package/scripts/generate-followup-session-contract.mjs +16 -0
  507. package/scripts/getcourse-text-source.spec.mjs +110 -0
  508. package/scripts/icp-import-verification/mutate.cjs +70 -0
  509. package/scripts/import-secret-read-contract.mjs +53 -0
  510. package/scripts/install-claude-desktop.js +1 -1
  511. package/scripts/lag-decisions.mjs +37 -0
  512. package/scripts/lag-decisions.spec.mjs +87 -0
  513. package/scripts/lag-open-task-owners.mjs +46 -0
  514. package/scripts/lag-open-task-owners.spec.mjs +78 -0
  515. package/scripts/lag-payload.mjs +10 -0
  516. package/scripts/legacy-action-lag.spec.mjs +22 -0
  517. package/scripts/legacy-field-refusal.shipped.spec.mjs +99 -0
  518. package/scripts/mcpcompat-handler.mjs +34 -0
  519. package/scripts/mcpcompat-loader.mjs +36 -0
  520. package/scripts/mcpcompat-typescript.mjs +3 -0
  521. package/scripts/platform-settings-shipped-parity.spec.mjs +89 -0
  522. package/scripts/prbr-round1-recheck.spec.mjs +605 -0
  523. package/scripts/prbr-shipped-compatibility.spec.mjs +276 -0
  524. package/scripts/publish-mcp-registry.mjs +98 -0
  525. package/scripts/publish-mcp-registry.spec.mjs +68 -0
  526. package/scripts/record-scheduled-task-call.mjs +26 -0
  527. package/scripts/refusal-probe.mjs +17 -0
  528. package/scripts/refusal-probe.spec.mjs +29 -0
  529. package/scripts/registry-diff.mjs +502 -0
  530. package/scripts/registry-diff.spec.mjs +787 -0
  531. package/scripts/request-query.mjs +43 -0
  532. package/scripts/request-query.spec.mjs +82 -0
  533. package/scripts/route-lag.mjs +23 -0
  534. package/scripts/route-lag.spec.mjs +30 -0
  535. package/scripts/sample-args.mjs +174 -0
  536. package/scripts/sample-args.spec.mjs +160 -0
  537. package/scripts/shipped-definitions.mjs +197 -0
  538. package/scripts/shipped-definitions.spec.mjs +57 -0
  539. package/scripts/shipped-inputs.spec.mjs +136 -0
  540. package/scripts/shipped-recordings.spec.mjs +19 -0
  541. package/scripts/shipped-ref.d.mts +5 -0
  542. package/scripts/shipped-ref.mjs +20 -0
  543. package/scripts/shipped-ref.spec.mjs +59 -0
  544. package/scripts/shipped-registry.mjs +107 -0
  545. package/scripts/shipped-sample-candidates.mjs +69 -0
  546. package/scripts/shipped-sample-candidates.spec.mjs +117 -0
  547. package/scripts/shipped-sample.mjs +871 -0
  548. package/scripts/shipped-sample.spec.mjs +451 -0
  549. package/scripts/sync-agent-model-schemas.mjs +72 -0
  550. package/scripts/sync-api-contracts.mjs +85 -0
  551. package/scripts/sync-api-contracts.spec.mjs +144 -0
  552. package/scripts/sync-contract-ledger-schemas.mjs +161 -0
  553. package/scripts/sync-crm-stage-pool-schema.mjs +85 -0
  554. package/scripts/sync-prompt-draft-schema.mjs +92 -0
  555. package/scripts/sync-route-map.mjs +94 -0
  556. package/scripts/sync-schema-descriptions.mjs +25 -0
  557. package/scripts/sync-tool-inventory.mjs +27 -0
  558. package/scripts/sync-trigger-argument-schema.mjs +97 -0
  559. package/scripts/sync-trigger-argument-schema.spec.mjs +91 -0
  560. package/scripts/sync-trigger-event-schema.mjs +20 -0
  561. package/scripts/sync-trigger-repeat-mapping.mjs +12 -0
  562. package/scripts/sync-trigger-task-schema.mjs +45 -0
  563. package/scripts/sync-voice-actions-contract.mjs +67 -0
  564. package/scripts/sync-voice-actions-tools.mjs +97 -0
  565. package/scripts/sync-wave-id-schemas.mjs +55 -0
  566. package/scripts/telegram-personal-confirmation-mutants.mjs +74 -0
  567. package/scripts/tool-input-schemas.mjs +46 -0
  568. package/scripts/tool-input-schemas.spec.mjs +50 -0
  569. package/scripts/verify-followup-adapters.mjs +235 -0
  570. package/scripts/verify-secret-read-import.mjs +84 -0
  571. package/scripts/verify-secret-read-mutants.mjs +143 -0
  572. package/dist/tools/definitions/analytics.d.ts +0 -40
  573. package/dist/tools/definitions/analytics.js +0 -233
  574. package/dist/tools/definitions/analytics.js.map +0 -1
  575. package/dist/tools/definitions/broadcast-mailing-variants.d.ts +0 -41
  576. package/dist/tools/definitions/broadcast-mailing-variants.js +0 -194
  577. package/dist/tools/definitions/broadcast-mailing-variants.js.map +0 -1
  578. package/dist/tools/definitions/dialogue-audit.d.ts +0 -38
  579. package/dist/tools/definitions/dialogue-audit.js +0 -292
  580. package/dist/tools/definitions/dialogue-audit.js.map +0 -1
  581. package/dist/tools/definitions/getcourse-wave.js +0 -519
  582. package/dist/tools/definitions/getcourse-wave.js.map +0 -1
  583. package/dist/tools/definitions/operational-overview.d.ts +0 -3
  584. package/dist/tools/definitions/operational-overview.js +0 -23
  585. package/dist/tools/definitions/operational-overview.js.map +0 -1
  586. package/dist/tools/definitions/scheduled-tasks.js +0 -173
  587. package/dist/tools/definitions/scheduled-tasks.js.map +0 -1
  588. package/dist/tools/definitions/trigger-contact-claims.d.ts +0 -7
  589. package/dist/tools/definitions/trigger-contact-claims.js +0 -60
  590. package/dist/tools/definitions/trigger-contact-claims.js.map +0 -1
  591. package/dist/tools/registry-mapping-cases-core.d.ts +0 -38
  592. package/dist/tools/registry-mapping-cases-core.js +0 -963
  593. package/dist/tools/registry-mapping-cases-core.js.map +0 -1
  594. package/dist/tools/registry-mapping-cases-platform.d.ts +0 -2
  595. package/dist/tools/registry-mapping-cases-platform.js +0 -1052
  596. package/dist/tools/registry-mapping-cases-platform.js.map +0 -1
  597. package/dist/tools/trigger-action-config.d.ts +0 -49
  598. package/dist/tools/trigger-action-config.js +0 -171
  599. package/dist/tools/trigger-action-config.js.map +0 -1
  600. package/docs/evals/model-behavior-scorecard.md +0 -80
  601. package/docs/operation-coverage.md +0 -114
  602. package/scripts/mock-credentials.js +0 -77
  603. package/scripts/model-eval-server.js +0 -213
  604. package/scripts/run-model-eval.js +0 -447
@@ -1,14 +1,32 @@
1
- // Read-only данные раздела «Аналитика коммуникации» (/communication-analytics), режим
2
- // «Исходящие». Не пересчитывать эти показатели по сообщениям:
1
+ // Read-only агрегаты страницы /broadcast-analytics. Не пересчитывать эти показатели по сообщениям:
3
2
  // backend уже держит когортную атрибуцию, delivery-гейт и дедуп событий между соседними волнами.
4
3
  import { z } from 'zod';
5
- import { defineTool, uuidSchema, withInstructions } from './shared.js';
6
- const calendarDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
7
- const filterValuesSchema = z.union([
8
- z.string().min(1),
9
- z.array(z.string().min(1)).min(1).max(50),
10
- ]);
11
- const channelSchema = z.enum([
4
+ import { MONEY_CURRENCY_INSTRUCTIONS, moneyCurrencySchema } from './money-currency.js';
5
+ import { BROADCAST_REASON_FILTER_VALUES } from '../../shared/constants/broadcast-reason.constants.js';
6
+ import { SCHEDULE_EXCLUSION_HINT, SCHEDULE_POOL_GROUPS_HINT } from './schedule-selection-hints.js';
7
+ import { MAILING_VARIANT_CONTENT_INSTRUCTIONS, mailingVariantContentBody, mailingVariantContentShape, } from './mailing-variant-content.js';
8
+ import { buildListQuery, compactObject, defineTool, uuidSchema, withInstructions, } from './shared.js';
9
+ const calendarDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/).refine((value) => {
10
+ const date = new Date(value + 'T00:00:00.000Z');
11
+ return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
12
+ }, 'Такой календарной даты не существует');
13
+ /** Потолок отгруженной схемы: сузить его значит внести отказ там, где клиент уже пишет длиннее. */
14
+ const MAILING_VARIANT_DESCRIPTION_MAX_LENGTH = 4000;
15
+ const paymentCandidatePagination = {
16
+ minPage: 1,
17
+ defaultPage: 1,
18
+ minPageSize: 1,
19
+ defaultPageSize: 30,
20
+ maxPageSize: 100,
21
+ };
22
+ /**
23
+ * ⛔ Регистр значений каналов у сторон РАЗНЫЙ, и это не косметика: у приёмной стороны включён
24
+ * запрет незнакомых значений, поэтому семь из одиннадцати отгруженных названий дают отказ.
25
+ * Перевод живёт ЗДЕСЬ, в конструкторе запроса, — единственном месте, через которое проходят все
26
+ * отчёты из общего перечисления.
27
+ */
28
+ // Этот набор сверяется с контрактом в wave-transports.spec.ts; трогая его, смотри туда.
29
+ export const SHIPPED_CHANNELS = [
12
30
  'Site',
13
31
  'Email',
14
32
  'SMS',
@@ -20,278 +38,52 @@ const channelSchema = z.enum([
20
38
  'Viber',
21
39
  'MAX',
22
40
  'UNKNOWN',
23
- ]);
24
- const preflightReasonSchema = z.enum([
25
- 'NO_MESSENGER',
26
- 'ACTIVE_INVOICE_SAME_PRODUCT',
27
- 'ACTIVE_DEAL_SAME_PRODUCT',
28
- 'PAID_SAME_PRODUCT',
29
- 'ACTIVE_DEAL_OTHER_PRODUCT',
30
- 'MANAGER_ASSIGNED_ACTIVE_DEAL',
31
- 'PRODUCT_CONTEXT_MISSING',
32
- 'GETCOURSE_CHECK_ERROR',
33
- 'ALREADY_SENT',
34
- 'USER_NOT_FOUND',
35
- ]);
36
- const actionTypeSchema = z.string().min(1);
37
- const PAYMENT_CANDIDATES_MAX_PAGE_SIZE = 100;
38
- const analyticsReportSchema = z.enum([
39
- 'summary',
40
- 'cohorts',
41
- 'skip_reasons',
42
- 'recipients',
43
- 'deals',
44
- 'action_failures',
45
- 'indirect_payments',
46
- 'response_windows',
47
- 'insights',
48
- 'recent_leads',
49
- 'data_quality',
50
- 'segments_catalog',
51
- 'funnel_products',
52
- 'payment_candidates',
53
- 'payment_candidates_summary',
54
- ]);
55
- const DRILLDOWN_REPORTS = new Set([
56
- 'recipients',
57
- 'deals',
58
- 'action_failures',
59
- 'indirect_payments',
60
- ]);
61
- const PAGED_REPORTS = new Set([
62
- ...DRILLDOWN_REPORTS,
63
- 'payment_candidates',
64
- ]);
65
- const DATED_REPORTS = new Set([
66
- ...DRILLDOWN_REPORTS,
67
- 'skip_reasons',
68
- ]);
69
- const isPaymentReport = (report) => report === 'payment_candidates' || report === 'payment_candidates_summary';
70
- const isCatalogReport = (report) => report === 'segments_catalog' || report === 'funnel_products';
41
+ ];
42
+ /** Шкала свежести базы — шесть корзин, как у сервера и у отгруженной копии моста. */
43
+ const FRESHNESS_BUCKETS = ['0-7', '8-14', '15-30', '31-45', '46-60', '61+'];
44
+ /**
45
+ * Источники «не отправлено» — те же, что у сервера (`reasonSource`). Без источника причина — код
46
+ * проверки перед волной, а поимённый список — проверка перед волной: прежний вход сохраняет смысл.
47
+ */
48
+ export const SKIP_REASON_SOURCES = [
49
+ 'WAVE_PREFLIGHT',
50
+ 'WAVE_RUN',
51
+ 'COLD_WELCOME',
52
+ 'SCHEDULED_RUN',
53
+ 'REGISTRATION_ANCHOR',
54
+ ];
55
+ /** Значение ИЛИ массив значений — форма отгруженной схемы у всех фильтров-измерений. */
56
+ const oneOrMany = (item) => z.union([item, z.array(item).min(1).max(50)]);
57
+ const asArray = (value) => value === undefined ? [] : Array.isArray(value) ? value : [value];
58
+ export const MECHANISMS = ['any', 'wave', 'cold_welcome'];
59
+ export const LAUNCH_SOURCES = ['manual', 'registration', 'schedule'];
71
60
  const analyticsQueryFields = {
61
+ currency: moneyCurrencySchema.optional(),
62
+ mechanism: oneOrMany(z.enum(MECHANISMS)).optional(),
63
+ launch_source: oneOrMany(z.enum(LAUNCH_SOURCES)).optional(),
72
64
  workspace_id: uuidSchema.optional(),
73
65
  mode: z.enum(['all', 'days', 'range']).default('days'),
74
66
  days: z.number().int().min(1).optional(),
75
67
  from: calendarDateSchema.optional(),
76
68
  to: calendarDateSchema.optional(),
77
69
  platform_id: uuidSchema.optional(),
78
- funnel: filterValuesSchema.optional(),
70
+ funnel: oneOrMany(z.string().min(1)).optional(),
79
71
  funnel_mode: z.literal('none').optional(),
80
- segment: filterValuesSchema.optional(),
81
- mailing_variant_id: z.union([uuidSchema, z.array(uuidSchema).min(1).max(50)]).optional(),
82
- /**
83
- * Механизм запуска. `wave` — доставляет процесс школы (мы штампуем поля сделки),
84
- * `cold_welcome` — пишем первыми сами. Оба значения = фильтра нет.
85
- */
86
- mechanism: z
87
- .union([z.enum(['wave', 'cold_welcome']), z.array(z.enum(['wave', 'cold_welcome'])).min(1).max(2)])
88
- .optional(),
89
- product: filterValuesSchema.optional(),
72
+ segment: oneOrMany(z.string().min(1)).optional(),
73
+ product: oneOrMany(z.string().min(1)).optional(),
90
74
  product_mode: z.literal('none').optional(),
91
- freshness: z
92
- .union([
93
- z.enum(['0-7', '8-14', '15-30', '31-45', '46-60', '61+']),
94
- z.array(z.enum(['0-7', '8-14', '15-30', '31-45', '46-60', '61+'])).min(1).max(6),
95
- ])
96
- .optional(),
97
- channels: z.union([channelSchema, z.array(channelSchema).min(1).max(11)]).optional(),
98
- /**
99
- * `entry` — период по дате входа в сегмент (регистрация, конец серии, предсписок), а не по
100
- * дате отправки. Строки без даты входа выпадают всегда, включая mode=all.
101
- */
102
- view: z.enum(['cohort', 'period', 'entry']).default('period'),
103
- };
104
- const analyticsToolSchema = z
105
- .object({
106
- ...analyticsQueryFields,
107
- report: analyticsReportSchema.default('summary'),
108
- date: calendarDateSchema.optional(),
109
- channel: channelSchema.optional(),
110
- import_job_id: z.string().min(1).optional(),
111
- page: z.number().int().min(1).optional(),
112
- page_size: z.number().int().min(1).max(200).optional(),
113
- recipient_metric: z
114
- .enum([
115
- 'sent',
116
- 'delivered',
117
- 'entered',
118
- 'deepDialog',
119
- 'handedOff',
120
- 'notDelivered',
121
- 'notSent',
122
- ])
123
- .optional(),
124
- deal_metric: z.enum(['issued', 'paid', 'withoutCohort']).optional(),
125
- reason: preflightReasonSchema.optional(),
126
- action_type: actionTypeSchema.optional(),
127
- payment_view: z.enum(['decidable', 'excluded', 'resolved']).optional(),
128
- payment_status: z.enum(['PENDING', 'CONFIRMED', 'CONFIRMED_INDIRECT', 'REJECTED']).optional(),
129
- payment_eligibility: z
130
- .enum([
131
- 'PENDING_CHECK',
132
- 'VERIFIED',
133
- 'VERIFIED_WEAK',
134
- 'UNVERIFIED',
135
- 'EXCLUDED_NON_AUTOWEB',
136
- 'EXCLUDED_PREEXISTING',
137
- ])
138
- .optional(),
139
- include_archived: z.boolean().optional(),
140
- })
141
- .strict()
142
- .superRefine((input, ctx) => {
143
- const addUnsupportedFilterIssue = (path, report) => {
144
- ctx.addIssue({
145
- code: 'custom',
146
- path: [path],
147
- message: `${path} не поддерживается для report=${report}`,
148
- });
149
- };
150
- if (input.report === 'recipients' && !input.recipient_metric) {
151
- ctx.addIssue({
152
- code: 'custom',
153
- path: ['recipient_metric'],
154
- message: 'recipient_metric обязателен для report=recipients',
155
- });
156
- }
157
- if (input.report === 'deals' && !input.deal_metric) {
158
- ctx.addIssue({
159
- code: 'custom',
160
- path: ['deal_metric'],
161
- message: 'deal_metric обязателен для report=deals',
162
- });
163
- }
164
- if (input.recipient_metric && input.report !== 'recipients') {
165
- addUnsupportedFilterIssue('recipient_metric', input.report);
166
- }
167
- if (input.deal_metric && input.report !== 'deals') {
168
- addUnsupportedFilterIssue('deal_metric', input.report);
169
- }
170
- if (input.reason &&
171
- (input.report !== 'recipients' || input.recipient_metric !== 'notSent')) {
172
- addUnsupportedFilterIssue('reason', input.report);
173
- }
174
- if (input.action_type && input.report !== 'action_failures') {
175
- addUnsupportedFilterIssue('action_type', input.report);
176
- }
177
- if (input.mode === 'range') {
178
- if (!input.from) {
179
- ctx.addIssue({
180
- code: 'custom',
181
- path: ['from'],
182
- message: 'from обязателен для mode=range',
183
- });
184
- }
185
- if (!input.to) {
186
- ctx.addIssue({
187
- code: 'custom',
188
- path: ['to'],
189
- message: 'to обязателен для mode=range',
190
- });
191
- }
192
- if (input.days !== undefined) {
193
- ctx.addIssue({
194
- code: 'custom',
195
- path: ['days'],
196
- message: 'days не поддерживается для mode=range',
197
- });
198
- }
199
- }
200
- else {
201
- if (input.from) {
202
- ctx.addIssue({
203
- code: 'custom',
204
- path: ['from'],
205
- message: `from не поддерживается для mode=${input.mode}`,
206
- });
207
- }
208
- if (input.to) {
209
- ctx.addIssue({
210
- code: 'custom',
211
- path: ['to'],
212
- message: `to не поддерживается для mode=${input.mode}`,
213
- });
214
- }
215
- if (input.mode === 'all' && input.days !== undefined) {
216
- ctx.addIssue({
217
- code: 'custom',
218
- path: ['days'],
219
- message: 'days не поддерживается для mode=all',
220
- });
221
- }
222
- }
223
- if (input.from && input.to && input.from > input.to) {
224
- ctx.addIssue({
225
- code: 'custom',
226
- path: ['to'],
227
- message: 'to не может быть раньше from',
228
- });
229
- }
230
- if ((input.page || input.page_size) && !PAGED_REPORTS.has(input.report)) {
231
- addUnsupportedFilterIssue(input.page ? 'page' : 'page_size', input.report);
232
- }
233
- if (input.date && !DATED_REPORTS.has(input.report)) {
234
- addUnsupportedFilterIssue('date', input.report);
235
- }
236
- if (input.import_job_id && !DATED_REPORTS.has(input.report)) {
237
- addUnsupportedFilterIssue('import_job_id', input.report);
238
- }
239
- if (input.channel && !DRILLDOWN_REPORTS.has(input.report)) {
240
- addUnsupportedFilterIssue('channel', input.report);
241
- }
242
- const hasAnalyticsOnlyFilter = input.days !== undefined ||
243
- input.from !== undefined ||
244
- input.to !== undefined ||
245
- input.platform_id !== undefined ||
246
- input.funnel !== undefined ||
247
- input.funnel_mode !== undefined ||
248
- input.segment !== undefined ||
249
- input.mailing_variant_id !== undefined ||
250
- input.mechanism !== undefined ||
251
- input.product !== undefined ||
252
- input.product_mode !== undefined ||
253
- input.freshness !== undefined ||
254
- input.channels !== undefined ||
255
- input.mode !== 'days' ||
256
- input.view !== 'period';
257
- if ((isPaymentReport(input.report) || isCatalogReport(input.report)) && hasAnalyticsOnlyFilter) {
258
- addUnsupportedFilterIssue('report', input.report);
259
- }
260
- const hasPaymentListFilter = input.payment_view !== undefined ||
261
- input.payment_status !== undefined ||
262
- input.payment_eligibility !== undefined ||
263
- input.include_archived !== undefined;
264
- if (hasPaymentListFilter && input.report !== 'payment_candidates') {
265
- addUnsupportedFilterIssue('payment_view', input.report);
266
- }
267
- if (input.report === 'payment_candidates' &&
268
- input.payment_view &&
269
- (input.payment_status || input.payment_eligibility)) {
270
- ctx.addIssue({
271
- code: 'custom',
272
- path: ['payment_view'],
273
- message: 'payment_view нельзя сочетать с payment_status/payment_eligibility',
274
- });
275
- }
276
- if (input.report === 'payment_candidates' &&
277
- input.page_size !== undefined &&
278
- input.page_size > PAYMENT_CANDIDATES_MAX_PAGE_SIZE) {
279
- ctx.addIssue({
280
- code: 'custom',
281
- path: ['page_size'],
282
- message: `page_size для report=payment_candidates не больше ${PAYMENT_CANDIDATES_MAX_PAGE_SIZE}`,
283
- });
284
- }
285
- });
286
- const appendFilterValues = (params, key, value) => {
287
- if (!value)
288
- return;
289
- const values = Array.isArray(value) ? value : [value];
290
- for (const item of values)
291
- params.append(key, item);
75
+ channels: oneOrMany(z.enum(SHIPPED_CHANNELS)).optional(),
76
+ freshness: oneOrMany(z.enum(FRESHNESS_BUCKETS)).optional(),
77
+ mailing_variant_id: oneOrMany(uuidSchema).optional(),
292
78
  };
293
- function buildAnalyticsParams(input) {
79
+ /** Отгруженное название канала → значение, которое принимает сервер. */
80
+ function toServerChannel(value) {
81
+ return value.toUpperCase();
82
+ }
83
+ function buildQuery(input) {
294
84
  const params = new URLSearchParams();
85
+ if (input.currency !== undefined)
86
+ params.set('currency', input.currency);
295
87
  const mode = input.mode ?? 'days';
296
88
  params.set('mode', mode);
297
89
  if (mode === 'days')
@@ -301,182 +93,692 @@ function buildAnalyticsParams(input) {
301
93
  if (input.to)
302
94
  params.set('to', input.to);
303
95
  if (input.platform_id)
304
- params.set('platformId', input.platform_id);
305
- appendFilterValues(params, 'funnel', input.funnel);
96
+ params.set('channelId', input.platform_id);
97
+ for (const value of asArray(input.funnel))
98
+ params.append('funnel', value);
306
99
  if (input.funnel_mode)
307
100
  params.set('funnelMode', input.funnel_mode);
308
- appendFilterValues(params, 'segment', input.segment);
309
- appendFilterValues(params, 'mailingVariantId', input.mailing_variant_id);
310
- appendFilterValues(params, 'mechanism', input.mechanism);
311
- appendFilterValues(params, 'product', input.product);
101
+ for (const value of asArray(input.segment))
102
+ params.append('segment', value);
103
+ for (const value of asArray(input.product))
104
+ params.append('product', value);
312
105
  if (input.product_mode)
313
106
  params.set('productMode', input.product_mode);
314
- appendFilterValues(params, 'freshness', input.freshness);
315
- appendFilterValues(params, 'channels', input.channels);
316
- params.set('view', input.view ?? 'period');
317
- return params;
318
- }
319
- function toQuery(params) {
107
+ for (const value of asArray(input.channels))
108
+ params.append('transports', toServerChannel(value));
109
+ for (const value of asArray(input.freshness))
110
+ params.append('freshness', value);
111
+ for (const value of asArray(input.mailing_variant_id))
112
+ params.append('mailingVariantId', value);
113
+ for (const value of asArray(input.mechanism).includes('any') ? [] : asArray(input.mechanism))
114
+ params.append('mechanism', value);
115
+ for (const value of asArray(input.launch_source))
116
+ params.append('launchSource', value);
117
+ if (input.view)
118
+ params.set('view', input.view);
320
119
  const query = params.toString();
321
120
  return query ? `?${query}` : '';
322
121
  }
323
- function appendDrilldownParams(params, input) {
324
- if (input.date)
325
- params.set('date', input.date);
326
- if (input.channel)
327
- params.set('channel', input.channel);
328
- if (input.import_job_id)
329
- params.set('importJobId', input.import_job_id);
330
- if (input.page)
331
- params.set('page', String(input.page));
332
- if (input.page_size)
333
- params.set('pageSize', String(input.page_size));
334
- }
335
- function buildPaymentCandidatesQuery(input) {
122
+ function buildPaymentCandidateQuery(input) {
336
123
  const params = new URLSearchParams();
337
- if (input.payment_view)
338
- params.set('view', input.payment_view);
339
- if (input.payment_status)
340
- params.set('status', input.payment_status);
341
- if (input.payment_eligibility)
342
- params.set('eligibility', input.payment_eligibility);
343
- if (input.include_archived)
344
- params.set('includeArchived', 'true');
345
- if (input.page)
346
- params.set('page', String(input.page));
347
- if (input.page_size)
348
- params.set('pageSize', String(input.page_size));
349
- return toQuery(params);
124
+ if (input.view)
125
+ params.set('view', input.view);
126
+ if (input.status)
127
+ params.set('status', input.status);
128
+ if (input.eligibility)
129
+ params.set('eligibility', input.eligibility);
130
+ params.set('includeArchived', String(input.include_archived ?? false));
131
+ params.set('page', String(input.page ?? paymentCandidatePagination.defaultPage));
132
+ params.set('pageSize', String(input.page_size ?? paymentCandidatePagination.defaultPageSize));
133
+ return `?${params.toString()}`;
350
134
  }
351
- function buildReportPath(input) {
352
- const workspaceBase = `/workspaces/${input.workspace_id}`;
353
- const analyticsBase = `${workspaceBase}/broadcast-analytics`;
354
- const params = buildAnalyticsParams(input);
355
- switch (input.report) {
356
- case 'summary':
357
- return `${analyticsBase}${toQuery(params)}`;
358
- case 'cohorts':
359
- return `${analyticsBase}/cohorts${toQuery(params)}`;
360
- case 'skip_reasons':
361
- if (input.import_job_id)
362
- params.set('importJobId', input.import_job_id);
363
- if (input.date)
364
- params.set('date', input.date);
365
- return `${analyticsBase}/skip-reasons${toQuery(params)}`;
366
- case 'recipients':
367
- appendDrilldownParams(params, input);
368
- params.set('metric', input.recipient_metric);
369
- if (input.reason)
370
- params.set('reason', input.reason);
371
- return `${analyticsBase}/recipients${toQuery(params)}`;
372
- case 'deals':
373
- appendDrilldownParams(params, input);
374
- params.set('metric', input.deal_metric);
375
- return `${analyticsBase}/deals${toQuery(params)}`;
376
- case 'action_failures':
377
- appendDrilldownParams(params, input);
378
- if (input.action_type)
379
- params.set('actionType', input.action_type);
380
- return `${analyticsBase}/action-failures${toQuery(params)}`;
381
- case 'indirect_payments':
382
- appendDrilldownParams(params, input);
383
- return `${analyticsBase}/indirect-payments${toQuery(params)}`;
384
- case 'response_windows':
385
- return `${analyticsBase}/response-windows${toQuery(params)}`;
386
- case 'insights':
387
- return `${analyticsBase}/insights${toQuery(params)}`;
388
- case 'recent_leads':
389
- return `${analyticsBase}/recent-leads${toQuery(params)}`;
390
- case 'data_quality':
391
- return `${analyticsBase}/data-quality${toQuery(params)}`;
392
- case 'segments_catalog':
393
- return `${analyticsBase}/segments`;
394
- case 'funnel_products':
395
- return `${analyticsBase}/funnel-products`;
396
- case 'payment_candidates':
397
- return `${workspaceBase}/payment-candidates${buildPaymentCandidatesQuery(input)}`;
398
- case 'payment_candidates_summary':
399
- return `${workspaceBase}/payment-candidates/summary`;
400
- }
135
+ /** Отчёты страницы аналитики. Каждый — свой адрес; выбор делает поле `report`. */
136
+ /**
137
+ * Отчёты фасада. ЭКСПОРТИРУЕТСЯ ради проверки полноты веток: она обязана брать перечень из КОДА,
138
+ * а не из опубликованного контракта. Отчёт, добавленный в код и забытый в контракте, иначе остался
139
+ * бы невидимым для проверки — при том, что фильтр аргументов его принимает.
140
+ */
141
+ export const ANALYTICS_REPORTS = [
142
+ 'summary',
143
+ 'registration_intake',
144
+ 'cohorts',
145
+ 'skip_reasons',
146
+ 'recipients',
147
+ 'deals',
148
+ 'action_failures',
149
+ 'indirect_payments',
150
+ 'response_windows',
151
+ 'insights',
152
+ 'recent_leads',
153
+ 'data_quality',
154
+ 'segments_catalog',
155
+ 'funnel_products',
156
+ 'payment_candidates',
157
+ 'payment_candidates_summary',
158
+ ];
159
+ /**
160
+ * ⛔ Отчёты, чей адрес лежит ВНЕ префикса аналитики. Слепой перенос «отчёт → суффикс» даёт им 404:
161
+ * два справочника живут прямо под воркспейсом, а оплаты — под своим разделом.
162
+ */
163
+ const REPORT_PATHS = {
164
+ registration_intake: '/broadcast-analytics/registration-intake',
165
+ summary: '/broadcast-analytics',
166
+ cohorts: '/broadcast-analytics/cohorts',
167
+ skip_reasons: '/broadcast-analytics/skip-reasons',
168
+ recipients: '/broadcast-analytics/recipients',
169
+ deals: '/broadcast-analytics/deals',
170
+ action_failures: '/broadcast-analytics/action-failures',
171
+ indirect_payments: '/broadcast-analytics/indirect-payments',
172
+ response_windows: '/broadcast-analytics/response-windows',
173
+ insights: '/broadcast-analytics/insights',
174
+ recent_leads: '/broadcast-analytics/recent-leads',
175
+ data_quality: '/broadcast-analytics/data-quality',
176
+ segments_catalog: '/broadcast-segments',
177
+ funnel_products: '/broadcast-funnel-products',
178
+ payment_candidates: '/payment-candidates',
179
+ payment_candidates_summary: '/payment-candidates/summary',
180
+ };
181
+ /**
182
+ * Отгруженное имя метрики получателей → имя, которое принимает сервер.
183
+ *
184
+ * ⛔ `notSent` в перевод НЕ входит: у отсеянных нет строки отправки, они читаются другим адресом.
185
+ * Отдать его метрикой значило бы получить отказ по незнакомому значению.
186
+ */
187
+ const RECIPIENT_METRIC_TO_SERVER = {
188
+ linkIssued: 'linkIssued',
189
+ clicked: 'clicked',
190
+ sent: 'sent',
191
+ delivered: 'received',
192
+ entered: 'enteredDialog',
193
+ deepDialog: 'deepDialog',
194
+ handedOff: 'handedOff',
195
+ notDelivered: 'notDelivered',
196
+ };
197
+ const SHIPPED_RECIPIENT_METRICS = [
198
+ 'linkIssued',
199
+ 'clicked',
200
+ 'sent',
201
+ 'delivered',
202
+ 'entered',
203
+ 'deepDialog',
204
+ 'handedOff',
205
+ 'notDelivered',
206
+ 'notSent',
207
+ ];
208
+ /** Степень доказанности оплаты; «слабо подтверждено» объявлено мёртвой осью и не доносится. */
209
+ const SHIPPED_PAYMENT_ELIGIBILITIES = [
210
+ 'PENDING_CHECK',
211
+ 'VERIFIED',
212
+ 'VERIFIED_WEAK',
213
+ 'UNVERIFIED',
214
+ 'EXCLUDED_NON_AUTOWEB',
215
+ 'EXCLUDED_PREEXISTING',
216
+ ];
217
+ /**
218
+ * Отчёты, у которых ЕСТЬ кликнутая ячейка: только им осмысленно передавать её координаты — день,
219
+ * канал и волну. У причин отсева канала не существует как понятия, и пробрасывать его туда значит
220
+ * ронять запрос на незнакомом параметре.
221
+ */
222
+ const CELL_REPORTS = new Set([
223
+ 'recipients',
224
+ 'deals',
225
+ 'action_failures',
226
+ 'indirect_payments',
227
+ ]);
228
+ const PAGINATED_REPORTS = new Set([
229
+ 'recipients',
230
+ 'deals',
231
+ 'action_failures',
232
+ 'indirect_payments',
233
+ 'payment_candidates',
234
+ ]);
235
+ /**
236
+ * Политика ответа — функция ОТЧЁТА, а не константа инструмента.
237
+ *
238
+ * Замеры: сводка 1,34 МБ (данные 897 КБ + итоги 437 КБ), когорты 635 КБ целиком в элементах.
239
+ * ⛔ У сводки выбрасываются ТОЛЬКО данные: прежняя редакция разрешала выбросить и итоги, и наружу
240
+ * уходил пустой отчёт под видом ответа.
241
+ */
242
+ function facadeResultPolicy(report) {
243
+ const narrowingHint = 'Сузьте отчёт: mode/days либо from+to на меньший период, platform_id на один канал, funnel/segment/product на один разрез.';
244
+ if (report === 'summary')
245
+ return { droppablePaths: ['data'], narrowingHint };
246
+ // У постраничных выдач механизм сужения уже есть: конверт подменил бы его тихой потерей.
247
+ if (PAGINATED_REPORTS.has(report))
248
+ return { neverTruncate: true };
249
+ if (report === 'cohorts')
250
+ return { neverTruncate: true, narrowingHint };
251
+ return { neverTruncate: true };
401
252
  }
402
- function reportInstructions(report) {
403
- if (isCatalogReport(report)) {
404
- return [
405
- 'Это workspace-scoped справочник страницы аналитики, поэтому период и dimension-фильтры к нему не применяются.',
406
- report === 'segments_catalog'
407
- ? 'description — сохранённое бизнес-правило отбора сегмента; null означает, что правило ещё не описано.'
408
- : 'unmappedKeys — нормализованные ключи воронок последних 90 дней, для которых продукт ещё не задан.',
409
- ];
253
+ function buildFacadePath(input) {
254
+ const report = input.report ?? 'summary';
255
+ const notSent = report === 'recipients' && input.recipient_metric === 'notSent';
256
+ const path = notSent ? '/broadcast-analytics/not-sent' : REPORT_PATHS[report];
257
+ if (report === 'payment_candidates' || report === 'payment_candidates_summary') {
258
+ // mode у оплат — раздел очереди, период имеет отдельное имя periodMode.
259
+ const params = new URLSearchParams(buildQuery(input).replace(/^\?/u, ''));
260
+ params.delete('view');
261
+ params.delete('mode');
262
+ const periodMode = input.mode ?? (input.days === undefined ? 'all' : 'days');
263
+ params.set('periodMode', periodMode);
264
+ if (periodMode !== 'days')
265
+ params.delete('days');
266
+ params.set('includeArchived', String(input.include_archived ?? false));
267
+ if (report === 'payment_candidates') {
268
+ if (input.payment_view)
269
+ params.set('view', input.payment_view);
270
+ if (input.payment_status)
271
+ params.set('status', input.payment_status);
272
+ if (input.payment_eligibility)
273
+ params.set('eligibility', input.payment_eligibility);
274
+ params.set('page', String(input.page ?? paymentCandidatePagination.defaultPage));
275
+ params.set('pageSize', String(input.page_size ?? paymentCandidatePagination.defaultPageSize));
276
+ }
277
+ const query = params.toString();
278
+ return `${path}${query ? `?${query}` : ''}`;
279
+ }
280
+ // Справочники разрезов фильтров не принимают вовсе: у них нет ни периода, ни измерений.
281
+ if (report === 'segments_catalog' || report === 'funnel_products')
282
+ return path;
283
+ const params = new URLSearchParams(buildQuery(input).replace(/^\?/u, ''));
284
+ // ⛔ Координаты кликнутой ЯЧЕЙКИ уходят только тем отчётам, у которых ячейка есть. Отгруженная
285
+ // схема плоская и разрешает прислать их с любым отчётом; пробросить их дальше значит уронить
286
+ // запрос целиком на незнакомом параметре — ровно тот отказ, который мы снимаем. Проверено
287
+ // прогоном отгруженного образца через настоящий вход маршрута: причины отсева канала не знают.
288
+ const cellReport = CELL_REPORTS.has(report);
289
+ if (cellReport || report === 'skip_reasons' || notSent) {
290
+ if (input.date)
291
+ params.set('date', input.date);
292
+ if (input.import_job_id)
293
+ params.set('importJobId', input.import_job_id);
294
+ }
295
+ if (cellReport && input.channel)
296
+ params.set('transport', toServerChannel(input.channel));
297
+ // Причина отсева осмысленна только там, где отсев и есть.
298
+ if ((report === 'skip_reasons' || notSent) && input.reason)
299
+ params.set('reason', input.reason);
300
+ // Источник задаёт и словарь причины, и то, чей поимённый список отдаётся.
301
+ if ((report === 'skip_reasons' || notSent) && input.reason_source) {
302
+ params.set('reasonSource', input.reason_source);
410
303
  }
411
- if (isPaymentReport(report)) {
412
- return [
413
- 'Это read-only данные вкладки «Подтверждение оплат». Общие фильтры аналитики к ней в UI не применяются; для списка используй payment_view либо payment_status/payment_eligibility и пагинацию.',
414
- 'Для payment_candidates максимальный page_size — 100.',
415
- 'Инструмент не подтверждает и не отклоняет оплаты: он только читает очередь и историю решений.',
416
- ];
304
+ if (report === 'action_failures' && input.action_type) {
305
+ params.set('actionType', input.action_type);
417
306
  }
418
- if (report === 'recipients' ||
419
- report === 'deals' ||
420
- report === 'action_failures' ||
421
- report === 'indirect_payments') {
422
- return [
423
- 'Это пагинированный drill-down той же страницы. Передавай те же общие фильтры, что использовались для агрегата, чтобы total совпадал с раскрываемой ячейкой.',
424
- 'Для следующей страницы увеличивай page; максимальный page_size — 200.',
425
- ];
307
+ if (report === 'recipients' && !notSent && input.recipient_metric) {
308
+ params.set('metric', RECIPIENT_METRIC_TO_SERVER[input.recipient_metric] ?? '');
426
309
  }
427
- return [
428
- 'Это authoritative данные раздела «Аналитика коммуникации» (/communication-analytics), режим «Исходящие». НЕ пересчитывай статистику рассылок через besales_dialogues_query или транскрипты.',
429
- 'Для «сегодня» используй mode=range, from=to=нужная дата YYYY-MM-DD; даты группируются по МСК.',
430
- ];
310
+ if (report === 'deals' && input.deal_metric)
311
+ params.set('metric', input.deal_metric);
312
+ if (PAGINATED_REPORTS.has(report) || notSent) {
313
+ params.set('page', String(input.page ?? 1));
314
+ if (input.page_size !== undefined)
315
+ params.set('pageSize', String(input.page_size));
316
+ }
317
+ const query = params.toString();
318
+ return `${path}${query ? `?${query}` : ''}`;
319
+ }
320
+ /** Общая часть — верна для любого отчёта фасада. */
321
+ const FACADE_COMMON_INSTRUCTIONS = [
322
+ ...MONEY_CURRENCY_INSTRUCTIONS,
323
+ 'Это authoritative-выдача страницы /broadcast-analytics. НЕ пересчитывай статистику рассылок через besales_dialogues_query или транскрипты.',
324
+ 'Для «сегодня» используй view=period, mode=range, from=to=нужная дата YYYY-MM-DD; даты группируются по МСК.',
325
+ 'funnel здесь — бизнес-воронка волны. Названия CRM pipeline читай отдельно через besales_crm_list + besales_crm_pipeline_list.',
326
+ ];
327
+ /** Подсказки по семействам отчётов; общее НЕ дублируется в каждой ветке. */
328
+ function facadeInstructions(report) {
329
+ const byReport = {
330
+ registration_intake: [
331
+ 'Состояние сверки отличает отказ внешнего реестра от нулевых регистраций; числа не получены — не ноль.',
332
+ 'В ответе: всего, вошли, не вошли, люди с группой, исходом приёма и приветствия, воронкой, моментом, номерами заказов и ссылкой на живой контакт.',
333
+ 'Результат напоминаний показывай отдельной подписью followupLabel; он не меняет исход приёма или доставки приветствия.',
334
+ 'Группа REENTRY_DEFERRED означает ожидание повтора, а не состоявшуюся отправку. Момент retryAt подписан сервером в часовом поясе платформы; используй reasonLabel.',
335
+ 'Сутки Москвы. Поздние вечерние регистрации и приветствия могут стать видны позже; внешний реестр отстаёт на 10 минут — 3 часа, редко сутки.',
336
+ 'Если два канала принимают одну воронку, недошедший заказ без решения роутера виден обоим. Эти числа нельзя складывать между каналами.',
337
+ ],
338
+ summary: [
339
+ 'Плитки прямой и косвенной выручки читают только moneyTotals.paidAmount и moneyTotals.indirectAmount за выбранный период и фильтры. Суммы data/totals/totalsByChannelId для плиток не складывать; поля potentialAmount в moneyTotals нет.',
340
+ 'Общий итог, кроме денежных плиток, бери из totals, а не суммой data по каналам: один получатель мультиканальной волны присутствует в нескольких channel-строках.',
341
+ 'view=entry группирует по настоящему моменту входа получателя в сегмент. entryCoverage (только на оси входа; на других осях null) показывает sentRows и paidDeals, а также rowsWithoutEntry и paidDealsWithoutEntry — сколько строк отправки и привязанных к волне оплат без настоящего входа выпали из дат. Возможные и косвенные оплаты в эти числа не входят.',
342
+ 'sent — строки получателей волн, не число реплик агента и не обязательно уникальные люди между разными волнами.',
343
+ 'received — доставленные строки получателей с учётом delivery-гейта канала.',
344
+ 'enteredDialog — получатели, у которых было хотя бы одно входящее после этой волны и до следующей; это НЕ количество входящих сообщений.',
345
+ 'handedOff — успешные GETCOURSE_CREATE_DEAL / GETCOURSE_SET_MANAGER / GETCOURSE_SET_DEAL_MANAGER / GETCOURSE_SET_PERSONAL_MANAGER. Назначение менеджера СДЕЛКИ без подходящей сделки — пропуск с причиной, а не успех: заявкой не считается (прежде считалось).',
346
+ ],
347
+ cohorts: [
348
+ 'Каждый items[] — отдельный запуск рассылки, а не строка сообщения или диалог.',
349
+ 'Список запусков и items[].date датируются запуском на любой оси view, в том числе view=entry; ось входа не передатирует запуск.',
350
+ 'label — имя запуска; funnel/segment/product/channels — снимок бизнес-разрезов волны.',
351
+ 'notSent у запуска — ТОЛЬКО проверка перед волной: notSent.total=null означает, что число неприменимо для этого запуска и выбранных фильтров; причину смотри в sources[].countAvailability.reasonLabel, это НЕ ноль. «Не отправлено» прогона волны (отказы самого прогона: цель статуса не нашлась, контакт не завёлся, сделка не перешла, исход неизвестен, прогон остановлен), холодного запуска, запуска по расписанию и автоприёма регистраций читай в sources[]: у каждого источника total (null — неизвестно или неприменимо, это НЕ ноль), tracedTotal и полнота countAvailability/detailsAvailability с причиной reasonLabel. Запуск «всё дошло» только если пусты ВСЕ источники — и только у долгого прогона: у одиночной волны источника «прогон волны» нет, её отказы импорта, статуса и сделки в sources[] не попадают (их показывает besales_getcourse_wave_status: failed и errorReasons).',
352
+ ],
353
+ skip_reasons: [
354
+ 'total, items и byDayProduct — сумма ИЗВЕСТНОГО по всем доступным источникам; у строки items есть source. Неизвестное в сумму не входит — его называет sources[]. Исключение — отказы одиночной волны (не долгого прогона): источника у них нет вовсе, и sources[] о них молчит; их показывает besales_getcourse_wave_status.',
355
+ 'unattributed — отсев волн без собственного значения выбранной воронки, сегмента или продукта: в total не входит, поимённого списка не имеет. unattributed=null означает «неприменимо», а не ноль.',
356
+ 'При фильтре мессенджера, сужающей свежести или view=entry отсев неприменим у ВСЕХ источников: смотри sources[].countAvailability.reasonLabel, не трактуй null как ноль.',
357
+ 'sources[]: total — сколько не отправлено (null — неизвестно, НЕ ноль); tracedTotal — сколько из них можно назвать поимённо; countAvailability/detailsAvailability — полнота числа и списка (COMPLETE/MISMATCH/PARTIAL/UNAVAILABLE) с причиной reasonLabel. Пересказывая, называй неполноту словами из reasonLabel, а не выдавай частичное число за полное.',
358
+ 'У проверки перед волной разбивка считает проверку по разу на КАЖДУЮ её причину: сумма по причинам больше числа проверок. Фильтр reason без reason_source относится только к проверке перед волной; с reason_source — к реестру этого источника.',
359
+ 'Причину называй подписью sources[].reasons[].label, а не кодом. У прогона волны «исход неизвестен» значит «сообщение могло уйти» — не пересказывай его как «не отправлено точно».',
360
+ ],
361
+ recipients: [
362
+ 'Это строки выбранной метрики за кликнутую ячейку, а не все получатели волны.',
363
+ 'У строк метрик получателей gcUserUrl и gcDealUrl — готовые адреса человека и заказа в GetCourse. null означает, что домен школы или идентификатор не доказан: адрес самостоятельно не составляй.',
364
+ 'recipient_metric=notSent отдаёт поимённый список ОДНОГО источника: по умолчанию — проверки перед волной; reason_source выбирает другой (WAVE_RUN — отказы самого прогона волны, COLD_WELCOME и SCHEDULED_RUN — журнал пропусков собственной отправки). У строк нет ни канала, ни статуса доставки, зато есть массив причин (коды — подписи бери из sources отчёта skip_reasons). Полноту списка называет detailsAvailability.',
365
+ ],
366
+ deals: [
367
+ 'withoutCohort — сделка без единой отметки волны, то есть оплата вне рассылок.',
368
+ 'gcUserUrl и gcDealUrl — готовые адреса человека и заказа в GetCourse. Школу заказа агента определяет его CRM-подключение, а не канал диалога. null означает, что домен или идентификатор не доказан: адрес самостоятельно не составляй.',
369
+ ],
370
+ response_windows: [
371
+ 'В conversionCycles поле cut различает overall — серверный итог стадии, product — строку продукта и segment — строку сегмента. Пустой ключ product или segment в строке разреза означает «без продукта» или «без сегмента», а не «Итого».',
372
+ 'Для строки «Итого» бери cut=overall. Медианы строк продукта и сегмента не складывай: сервер считает медиану итога отдельно. Цикл оплаты начинается с первого получения человеком логической волны; оплату до получения сервер исключает.',
373
+ ],
374
+ action_failures: [
375
+ 'Показаны только production-сбои в окне волны; разбивка идёт по ключу действия, заданному настройщиком.',
376
+ 'gcUserUrl и gcDealUrl — готовые адреса человека и закреплённого заказа в GetCourse. null означает, что домен школы или идентификатор не доказан: адрес самостоятельно не составляй.',
377
+ ],
378
+ data_quality: [
379
+ 'paymentsWithoutCohort приходит ПУСТЫМ при фильтрах воронки, сегмента, продукта, варианта рассылки, волны или свежести: у оплаты без волны этих измерений нет, и число под таким фильтром было бы неправдой.',
380
+ 'noMessenger=null означает, что отсев без мессенджера неприменим при фильтре мессенджера, сужающей свежести или view=entry; это НЕ ноль.',
381
+ 'total — строки отправки за период, а не уникальные люди.',
382
+ ],
383
+ recent_leads: [
384
+ 'Окно считается по дате ОТВЕТА, а не по дате отправки волны; отдаётся до пятидесяти последних.',
385
+ ],
386
+ payment_candidates: [
387
+ 'Это read-only отчёт. Решения по оплатам принимает только вошедший человек в интерфейсе Besales.',
388
+ 'payment_view перекрывает payment_status и payment_eligibility; для точечных фильтров не передавай payment_view.',
389
+ 'Общие фильтры оплат передаются списку и сводке; период — по дате оплаты. Без mode/days — всё время; mode=days без days — последние 30 дней.',
390
+ ],
391
+ payment_candidates_summary: [
392
+ 'Счётчики учитывают те же общие фильтры оплат; include_archived=true включает архивные строки, по умолчанию только активные.',
393
+ ],
394
+ segments_catalog: ['Справочник разрезов; фильтры периода и измерений он не принимает.'],
395
+ funnel_products: ['Справочник разрезов; фильтры периода и измерений он не принимает.'],
396
+ };
397
+ return [...FACADE_COMMON_INSTRUCTIONS, ...(byReport[report] ?? [])];
431
398
  }
432
399
  export const broadcastAnalyticsTools = [
433
400
  defineTool({
434
401
  name: 'besales_broadcast_analytics_get',
435
402
  sideEffect: 'none',
403
+ closedWorld: true,
404
+ // Отчёты получателей, сделок, отсеянных и отказов действий несут имена и идентификаторы людей.
405
+ carriesCustomerData: true,
436
406
  requiredFields: [],
437
407
  defaultWorkspaceId: true,
438
- resultPolicy: {
439
- // `totals` — авторитетный итог отчёта; его нельзя выбрасывать вместе со строками.
440
- droppablePaths: ['data'],
441
- narrowingHint: 'Сузьте отчёт: mode/days либо from+to на меньший период, platform_id на один канал, funnel/segment/product на один разрез.',
442
- },
443
- schema: analyticsToolSchema,
408
+ // Самый тяжёлый замеренный отчёт — 1,34 МБ, из них data 897 КБ и totals 437 КБ. Сужать
409
+ // отчёт есть чем, поэтому при превышении выбрасываются именно эти два поля, а справочники
410
+ // разрезов остаются: по ним и строится следующий, суженный запрос.
411
+ // ⚠️ Выбрасывается ТОЛЬКО `data`. Прежняя редакция разрешала выбросить ещё и `totals`,
412
+ // и на замеренном ответе в 1,34 МБ срабатывали оба шага подряд: наружу уходил конверт без
413
+ // строк И без итогов — то есть пустой отчёт под видом ответа. Итоги вдвое легче строк и
414
+ // несут главное; если и без строк не влезло — честнее отказ с подсказкой, чем пустота.
415
+ resultPolicy: (input) => facadeResultPolicy(input.report ?? 'summary'),
416
+ schema: z
417
+ .object({
418
+ ...analyticsQueryFields,
419
+ // У оплат без периода — всё время; у аналитики — последние 30 дней.
420
+ mode: z.enum(['all', 'days', 'range']).optional(),
421
+ view: z.enum(['cohort', 'entry', 'period']).default('period'),
422
+ report: z.enum(ANALYTICS_REPORTS).default('summary'),
423
+ date: calendarDateSchema.optional(),
424
+ channel: z.enum(SHIPPED_CHANNELS).optional(),
425
+ // ⛔ Сужено до идентификатора намеренно: сервер принимает только его, и отказ на разборе
426
+ // называет поле и формат, тогда как отказ сервера приходит без подсказки.
427
+ import_job_id: uuidSchema.optional(),
428
+ page: z.number().int().min(1).optional(),
429
+ page_size: z.number().int().min(1).max(200).optional(),
430
+ recipient_metric: z.enum(SHIPPED_RECIPIENT_METRICS).optional(),
431
+ deal_metric: z.enum(['issued', 'paid', 'confirmedByOperator', 'withoutCohort']).optional(),
432
+ reason: z.enum(BROADCAST_REASON_FILTER_VALUES).optional(),
433
+ reason_source: z.enum(SKIP_REASON_SOURCES).optional(),
434
+ action_type: z.string().min(1).optional(),
435
+ payment_view: z.enum(['decidable', 'excluded', 'resolved']).optional(),
436
+ payment_status: z
437
+ .enum(['PENDING', 'CONFIRMED', 'CONFIRMED_INDIRECT', 'REJECTED'])
438
+ .optional(),
439
+ payment_eligibility: z.enum(SHIPPED_PAYMENT_ELIGIBILITIES).optional(),
440
+ include_archived: z.boolean().optional(),
441
+ })
442
+ .strict()
443
+ .superRefine((value, ctx) => {
444
+ const unsupported = (field) => {
445
+ ctx.addIssue({ code: 'custom', path: [field], message: field + ' не поддерживается для report=' + value.report });
446
+ };
447
+ const payment = ['payment_candidates', 'payment_candidates_summary'].includes(value.report);
448
+ const catalog = ['segments_catalog', 'funnel_products'].includes(value.report);
449
+ const dated = CELL_REPORTS.has(value.report) || value.report === 'skip_reasons';
450
+ const notSent = value.report === 'recipients' && value.recipient_metric === 'notSent';
451
+ if (value.mode === 'range') {
452
+ if (value.days !== undefined)
453
+ unsupported('days');
454
+ if (payment && value.from === undefined && value.to === undefined) {
455
+ ctx.addIssue({ code: 'custom', path: ['from'], message: 'mode=range для оплат требует хотя бы одну границу from или to' });
456
+ }
457
+ }
458
+ else {
459
+ for (const field of ['from', 'to'])
460
+ if (value[field] !== undefined)
461
+ unsupported(field);
462
+ if (value.mode === 'all' && value.days !== undefined)
463
+ unsupported('days');
464
+ }
465
+ if (value.from !== undefined && value.to !== undefined && value.from > value.to) {
466
+ ctx.addIssue({ code: 'custom', path: ['to'], message: 'to не может быть раньше from' });
467
+ }
468
+ if (value.recipient_metric !== undefined && value.report !== 'recipients')
469
+ unsupported('recipient_metric');
470
+ if (value.deal_metric !== undefined && value.report !== 'deals')
471
+ unsupported('deal_metric');
472
+ if (!notSent && value.report !== 'skip_reasons') {
473
+ for (const field of ['reason', 'reason_source'])
474
+ if (value[field] !== undefined)
475
+ unsupported(field);
476
+ }
477
+ if (value.action_type !== undefined && value.report !== 'action_failures')
478
+ unsupported('action_type');
479
+ if (!PAGINATED_REPORTS.has(value.report)) {
480
+ for (const field of ['page', 'page_size'])
481
+ if (value[field] !== undefined)
482
+ unsupported(field);
483
+ }
484
+ if (!dated) {
485
+ for (const field of ['date', 'import_job_id'])
486
+ if (value[field] !== undefined)
487
+ unsupported(field);
488
+ }
489
+ if (value.channel !== undefined && !CELL_REPORTS.has(value.report))
490
+ unsupported('channel');
491
+ if (catalog) {
492
+ for (const field of ['days', 'from', 'to', 'platform_id', 'funnel', 'funnel_mode', 'segment', 'product', 'product_mode', 'channels', 'freshness', 'mailing_variant_id']) {
493
+ if (value[field] !== undefined)
494
+ unsupported(field);
495
+ }
496
+ if (value.mode !== undefined && value.mode !== 'days')
497
+ unsupported('mode');
498
+ if (value.view !== 'period')
499
+ unsupported('view');
500
+ for (const field of ['mechanism', 'launch_source'])
501
+ if (value[field] !== undefined)
502
+ unsupported(field);
503
+ }
504
+ if (payment && value.view !== 'period')
505
+ unsupported('view');
506
+ if (!payment && value.include_archived !== undefined)
507
+ unsupported('include_archived');
508
+ if (value.report !== 'payment_candidates') {
509
+ for (const field of ['payment_view', 'payment_status', 'payment_eligibility'])
510
+ if (value[field] !== undefined)
511
+ unsupported(field);
512
+ }
513
+ if (value.report === 'payment_candidates' && value.page_size !== undefined && value.page_size > paymentCandidatePagination.maxPageSize) {
514
+ ctx.addIssue({ code: 'custom', path: ['page_size'], message: 'page_size для report=payment_candidates не больше 100' });
515
+ }
516
+ if (value.currency !== undefined &&
517
+ ['segments_catalog', 'funnel_products', 'payment_candidates', 'payment_candidates_summary']
518
+ .includes(value.report)) {
519
+ ctx.addIssue({
520
+ code: 'custom',
521
+ path: ['currency'],
522
+ message: 'Этот отчёт не поддерживает валютный фильтр',
523
+ });
524
+ }
525
+ if (['payment_candidates', 'payment_candidates_summary'].includes(value.report) &&
526
+ value.launch_source !== undefined) {
527
+ ctx.addIssue({
528
+ code: z.ZodIssueCode.custom,
529
+ path: ['launch_source'],
530
+ message: 'В отдельной очереди оплат отбор по источнику запуска пока недоступен',
531
+ });
532
+ }
533
+ if (value.report === 'registration_intake' &&
534
+ value.view !== undefined &&
535
+ value.view !== 'period') {
536
+ ctx.addIssue({
537
+ code: 'custom',
538
+ path: ['view'],
539
+ message: 'Для регистраций доступен только период регистрации',
540
+ });
541
+ }
542
+ if (value.report === 'recipients' && value.recipient_metric === undefined) {
543
+ ctx.addIssue({
544
+ code: z.ZodIssueCode.custom,
545
+ path: ['recipient_metric'],
546
+ message: 'report=recipients требует recipient_metric',
547
+ });
548
+ }
549
+ if (value.report === 'deals' && value.deal_metric === undefined) {
550
+ ctx.addIssue({
551
+ code: z.ZodIssueCode.custom,
552
+ path: ['deal_metric'],
553
+ message: 'report=deals требует deal_metric',
554
+ });
555
+ }
556
+ // ⛔ Отвергается ЗДЕСЬ, с причиной. Транслировать значение серверу нельзя: у него такой оси
557
+ // нет, и клиент получил бы отказ без объяснения.
558
+ if (value.payment_eligibility === 'VERIFIED_WEAK') {
559
+ ctx.addIssue({
560
+ code: z.ZodIssueCode.custom,
561
+ path: ['payment_eligibility'],
562
+ message: 'VERIFIED_WEAK больше не существует: в проде у этого значения не было ни одного ' +
563
+ 'писателя, ось всегда была пуста и упразднена. Используйте VERIFIED или UNVERIFIED.',
564
+ });
565
+ }
566
+ }),
444
567
  createRequest: (input) => ({
445
568
  method: 'GET',
446
- path: buildReportPath(input),
569
+ path: `/workspaces/${input.workspace_id}${buildFacadePath(input)}`,
447
570
  }),
448
- enhanceResponse: (response, input) => withInstructions(response, [
449
- ...reportInstructions(input.report),
450
- ...(isPaymentReport(input.report)
451
- ? []
452
- : [
453
- 'summary: общий итог бери из totals, а не суммой data по каналам — один получатель мультиканальной волны присутствует в нескольких channel-строках.',
454
- 'enteredDialog/entered — ответившие получатели в окне волны, не число входящих сообщений.',
455
- 'funnel — бизнес-воронка волны; названия CRM pipeline при необходимости читай через besales_crm_list + besales_crm_pipeline_list.',
456
- ]),
457
- ]),
571
+ // ⛔ Подсказки — ФУНКЦИЯ отчёта, как и политика усечения. Прежняя редакция объясняла только
572
+ // сводку; после возврата выбора она врала бы про оплаты, справочники и постраничные списки —
573
+ // рассказывала бы про totals и мультиканальные строки там, где ответ устроен иначе.
574
+ enhanceResponse: (response, input) => withInstructions(response, facadeInstructions(input.report ?? 'summary')),
458
575
  }),
459
576
  defineTool({
460
577
  name: 'besales_broadcast_cohorts_list',
461
578
  sideEffect: 'none',
579
+ closedWorld: true,
462
580
  requiredFields: [],
463
581
  defaultWorkspaceId: true,
582
+ // Замерено 635 КБ, и весь вес — в items. Выбросить их значит вернуть пустой конверт,
583
+ // поэтому здесь честнее отказ с указанием, чем сузить, чем ответ без содержимого.
464
584
  resultPolicy: {
465
585
  neverTruncate: true,
466
586
  narrowingHint: 'Сузьте выборку: mode/days либо from+to на меньший период, platform_id на один канал, funnel/segment/product на один разрез.',
467
587
  },
468
- schema: z.object(analyticsQueryFields).strict(),
588
+ schema: z
589
+ .object({
590
+ ...analyticsQueryFields,
591
+ view: z.enum(['cohort', 'entry', 'period']).default('period'),
592
+ })
593
+ .strict(),
469
594
  createRequest: (input) => ({
470
595
  method: 'GET',
471
- path: buildReportPath({ ...input, report: 'cohorts' }),
596
+ path: `/workspaces/${input.workspace_id}/broadcast-analytics/cohorts${buildQuery(input)}`,
472
597
  }),
473
598
  enhanceResponse: (response) => withInstructions(response, [
474
- 'Backward-compatible alias для besales_broadcast_analytics_get с report=cohorts.',
599
+ ...MONEY_CURRENCY_INSTRUCTIONS,
475
600
  'Каждый items[] — отдельный запуск рассылки (importJobId/cohortId), а не строка сообщения или диалог.',
476
- 'Для общей статистики и остальных вкладок используй besales_broadcast_analytics_get.',
601
+ 'Список запусков и items[].date датируются запуском на любой оси view, в том числе view=entry; ось входа не передатирует запуск.',
602
+ 'label — имя запуска; funnel/segment/product/channels — снимок бизнес-разрезов волны.',
603
+ 'notSent у запуска — ТОЛЬКО проверка перед волной: notSent.total=null означает, что число неприменимо для этого запуска и выбранных фильтров; причину смотри в sources[].countAvailability.reasonLabel, это НЕ ноль. Для других источников «не отправлено» смотри sources[].total, tracedTotal и countAvailability/detailsAvailability; null не считай нулём.',
604
+ 'sent/delivered/entered/handedOff считаются по получателям этой волны; entered — ответившие получатели, не число их сообщений.',
605
+ 'Для общей статистики по дням используй besales_broadcast_analytics_get; транскрипты нужны только для качественного разбора конкретных диалогов.',
606
+ ]),
607
+ }),
608
+ defineTool({
609
+ name: 'besales_broadcast_payment_candidates_list',
610
+ // Постраничная выдача уже работает: конверт подменил бы исправный механизм тихой потерей.
611
+ resultPolicy: { neverTruncate: true },
612
+ sideEffect: 'none',
613
+ closedWorld: true,
614
+ // Отдаёт имя человека, его идентификатор в кабинете школы и ссылку на карточку. Пометка была
615
+ // пропущена с самого начала: содержимое то же, что у фасада с отчётом оплат.
616
+ carriesCustomerData: true,
617
+ requiredFields: [],
618
+ defaultWorkspaceId: true,
619
+ schema: z
620
+ .object({
621
+ workspace_id: uuidSchema.optional(),
622
+ view: z.enum(['decidable', 'excluded', 'resolved']).optional(),
623
+ status: z.enum(['PENDING', 'CONFIRMED', 'CONFIRMED_INDIRECT', 'REJECTED']).optional(),
624
+ eligibility: z
625
+ .enum([
626
+ 'PENDING_CHECK',
627
+ 'VERIFIED',
628
+ 'UNVERIFIED',
629
+ 'EXCLUDED_NON_AUTOWEB',
630
+ 'EXCLUDED_PREEXISTING',
631
+ ])
632
+ .optional(),
633
+ include_archived: z.boolean().default(false),
634
+ page: z
635
+ .number()
636
+ .int()
637
+ .min(paymentCandidatePagination.minPage)
638
+ .default(paymentCandidatePagination.defaultPage),
639
+ page_size: z
640
+ .number()
641
+ .int()
642
+ .min(paymentCandidatePagination.minPageSize)
643
+ .max(paymentCandidatePagination.maxPageSize)
644
+ .default(paymentCandidatePagination.defaultPageSize),
645
+ })
646
+ .strict(),
647
+ createRequest: (input) => ({
648
+ method: 'GET',
649
+ path: `/workspaces/${input.workspace_id}/payment-candidates${buildPaymentCandidateQuery(input)}`,
650
+ }),
651
+ enhanceResponse: (response) => withInstructions(response, [
652
+ 'Это read-only отчёт. Решения по оплатам принимаются только вошедшим человеком в интерфейсе Besales.',
653
+ 'view перекрывает status и eligibility; для точечных фильтров не передавай view.',
654
+ ]),
655
+ }),
656
+ defineTool({
657
+ name: 'besales_broadcast_payment_candidates_summary',
658
+ sideEffect: 'none',
659
+ closedWorld: true,
660
+ requiredFields: [],
661
+ defaultWorkspaceId: true,
662
+ schema: z.object({ workspace_id: uuidSchema.optional() }).strict(),
663
+ createRequest: (input) => ({
664
+ method: 'GET',
665
+ path: `/workspaces/${input.workspace_id}/payment-candidates/summary`,
666
+ }),
667
+ enhanceResponse: (response) => withInstructions(response, [
668
+ 'Счётчики охватывают только активные строки: решаемые, исключённые и уже решённые.',
669
+ 'Это read-only отчёт; мост не принимает денежные решения.',
670
+ ]),
671
+ }),
672
+ defineTool({
673
+ name: 'besales_broadcast_mailing_variant_list',
674
+ sideEffect: 'none',
675
+ requiredFields: [],
676
+ defaultWorkspaceId: true,
677
+ schema: z
678
+ .object({
679
+ workspace_id: uuidSchema.optional(),
680
+ include_archived: z.boolean().default(false),
681
+ })
682
+ .strict(),
683
+ createRequest: (input) => ({
684
+ method: 'GET',
685
+ path: `/workspaces/${input.workspace_id}/broadcast-mailing-variants?includeArchived=${input.include_archived}`,
686
+ }),
687
+ }),
688
+ defineTool({
689
+ name: 'besales_broadcast_mailing_variant_create',
690
+ sideEffect: 'write',
691
+ requiredFields: ['name'],
692
+ defaultWorkspaceId: true,
693
+ schema: z
694
+ .object({
695
+ workspace_id: uuidSchema.optional(),
696
+ name: z.string().min(1).max(200),
697
+ description: z.string().max(MAILING_VARIANT_DESCRIPTION_MAX_LENGTH).optional(),
698
+ ...mailingVariantContentShape,
699
+ })
700
+ .strict(),
701
+ createRequest: (input) => ({
702
+ method: 'POST',
703
+ path: `/workspaces/${input.workspace_id}/broadcast-mailing-variants`,
704
+ body: compactObject({
705
+ name: input.name,
706
+ description: input.description,
707
+ ...mailingVariantContentBody(input),
708
+ }),
709
+ }),
710
+ enhanceResponse: (response) => withInstructions(response, [
711
+ 'Создаёт вариант или возвращает из архива вариант с тем же именем. Возврат из архива ОЧИЩАЕТ прежние тексты по сегментам, если message_text_by_segment не передан: иначе часть людей получила бы прошлые тексты, невидимые в этом вызове.',
712
+ ...MAILING_VARIANT_CONTENT_INSTRUCTIONS,
713
+ ]),
714
+ }),
715
+ defineTool({
716
+ name: 'besales_broadcast_mailing_variant_update',
717
+ sideEffect: 'write',
718
+ // ⛔ Имя НЕобязательно: правка частичная. Требование имени отвергало бы «верни из архива» и
719
+ // «поправь только пояснение», а присланное клиентом старое имя переписало бы снимки когорт.
720
+ requiredFields: ['mailing_variant_id'],
721
+ defaultWorkspaceId: true,
722
+ schema: z
723
+ .object({
724
+ workspace_id: uuidSchema.optional(),
725
+ mailing_variant_id: uuidSchema,
726
+ name: z.string().min(1).max(200).optional(),
727
+ description: z.string().max(MAILING_VARIANT_DESCRIPTION_MAX_LENGTH).nullable().optional(),
728
+ archived: z.boolean().optional(),
729
+ explicit_confirmation: z.literal(true).optional(),
730
+ ...mailingVariantContentShape,
731
+ })
732
+ .strict()
733
+ .superRefine((value, ctx) => {
734
+ // Архивация — опасное действие: вариант исчезает из выдачи по умолчанию. Подтверждение
735
+ // требуется ИМЕННО на ней, а не на всей правке: переименование ничего не прячет.
736
+ if (value.archived === true && value.explicit_confirmation !== true) {
737
+ ctx.addIssue({
738
+ code: z.ZodIssueCode.custom,
739
+ message: 'archived=true требует explicit_confirmation=true после подтверждения человека',
740
+ });
741
+ }
742
+ }),
743
+ createRequest: (input) => ({
744
+ method: 'PATCH',
745
+ path: `/workspaces/${input.workspace_id}/broadcast-mailing-variants/${input.mailing_variant_id}`,
746
+ body: compactObject({
747
+ name: input.name,
748
+ description: input.description,
749
+ archived: input.archived,
750
+ ...mailingVariantContentBody(input),
751
+ }),
752
+ }),
753
+ enhanceResponse: (response) => withInstructions(response, [...MAILING_VARIANT_CONTENT_INSTRUCTIONS]),
754
+ }),
755
+ defineTool({
756
+ name: 'besales_broadcast_mailing_variant_delete',
757
+ sideEffect: 'write',
758
+ requiredFields: ['mailing_variant_id', 'explicit_confirmation'],
759
+ defaultWorkspaceId: true,
760
+ schema: z
761
+ .object({
762
+ workspace_id: uuidSchema.optional(),
763
+ mailing_variant_id: uuidSchema,
764
+ explicit_confirmation: z.literal(true),
765
+ })
766
+ .strict(),
767
+ createRequest: (input) => ({
768
+ method: 'DELETE',
769
+ path: `/workspaces/${input.workspace_id}/broadcast-mailing-variants/${input.mailing_variant_id}`,
770
+ }),
771
+ enhanceResponse: (response) => withInstructions(response, [
772
+ 'Вариант только архивирован (`archivedAt`), а не удалён физически: hard-delete запрещён внешним ключом. Запись остаётся доступна через besales_broadcast_mailing_variant_list с include_archived=true.',
477
773
  ]),
478
774
  }),
479
775
  ];
776
+ /**
777
+ * История запусков рассылок одним списком — четыре источника строк.
778
+ *
779
+ * ⛔ Схема — отгруженная без расхождения; тексты для модели — по ответу v2: код и подпись словами рядом, прогон
780
+ * называется «взято · отправлено» теми же числами, что у «Пула сегодня», остановка строки — полем `stop`.
781
+ */
480
782
  export const broadcastLaunchTools = [
481
783
  defineTool({
482
784
  name: 'besales_broadcast_launches_list',
@@ -488,36 +790,35 @@ export const broadcastLaunchTools = [
488
790
  workspace_id: uuidSchema.optional(),
489
791
  mailing_variant_id: uuidSchema.optional(),
490
792
  schedule_id: uuidSchema.optional(),
793
+ origin: z.enum(['MANUAL', 'WAVE', 'SCHEDULE', 'REGISTRATION', 'CAMPAIGN']).optional(),
794
+ cursor: z.string().max(512).optional(),
491
795
  limit: z.number().int().min(1).max(50).optional(),
492
796
  })
493
797
  .strict(),
494
- createRequest: (input) => {
495
- const query = new URLSearchParams();
496
- if (input.mailing_variant_id)
497
- query.set('mailingVariantId', input.mailing_variant_id);
498
- if (input.schedule_id)
499
- query.set('scheduleId', input.schedule_id);
500
- if (input.limit)
501
- query.set('limit', String(input.limit));
502
- const suffix = query.toString();
503
- return {
504
- method: 'GET',
505
- path: `/workspaces/${input.workspace_id}/broadcasts/launches${suffix ? `?${suffix}` : ''}`,
506
- };
507
- },
798
+ createRequest: (input) => ({
799
+ method: 'GET',
800
+ path: `/workspaces/${input.workspace_id}/broadcasts/launches` +
801
+ buildListQuery({
802
+ mailingVariantId: input.mailing_variant_id,
803
+ scheduleId: input.schedule_id,
804
+ limit: input.limit,
805
+ origin: input.origin,
806
+ cursor: input.cursor,
807
+ }),
808
+ }),
508
809
  enhanceResponse: (response) => withInstructions(response, [
509
- 'История запусков ЧЕТЫРЁХ видов. kind=cold_welcome — наша отправка (messageSnapshot = что реально ушло), kind=wave — волна по файлу (текст доставляет процесс школы). Прогон расписания и автоприём регистраций — тоже kind=cold_welcome: их различают НЕ по kind, а по наличию scheduleRun и registrationDay.',
510
- '⚠️⚠️ Прогон расписания — строка с scheduleRun: механизм тот же cold_welcome (пишем первыми сами), различает их то, ЧТО запустило отправку. У такой строки totalContacts — это ПУЛ до исключений (а не размер когорты), sentCount — фактически доставленные, failedCount ВСЕГДА 0, outcomes ВСЕГДА null: разницу объясняют scheduleRun.selection и scheduleRun.facts (и reason).',
511
- '⚠️⚠️ Числа прогона называй так же, как вкладки «История» и «Пул сегодня»: в строке «взято · отправлено», а план по людям — scheduleRun.dayBreakdown: расклад сегмента на день прогона из утреннего расчёта «Пула сегодня», те же числа, что вкладка «Пул» показывает у сегмента на этот день (registrations — регистраций, вошедших в сегмент от прошлого запуска; people — разных людей среди них; excluded — людей по группам отсева, словарь как у selection.excluded; selected — уйдёт из людей дня; carriedOver — уйдёт людей с прошлых дней; посчитан до запуска, поэтому selected + carriedOver может расходиться с selection.taken; null — утро этот сегмент не посчитало, плана нет, а не ноль). Пул запуска человеку людьми дня не называй: это регистрации всего окна свежести, почти всем из которых уже писали. scheduleRun.poolSize — пул запуска: регистрации в окне свежести авторассылки, которые запуск рассмотрел (не люди: человек с несколькими регистрациями посчитан несколько раз; null — пул ещё не измерен). scheduleRun.selection — те же числа, что facts.scheduleRuns у besales_broadcast_pool_today: collapsed — несколько регистраций у одного человека (poolSize − collapsed = людей в пуле); excluded — людей по группам отсева (noMessenger — нет мессенджера (в том числе прошлая подача этим расписанием закончилась «карточку не завели» или «каналами не достать»), inContact — на связи или писали недавно (этим сегментом уже писали, идёт разговор или трое суток тишины после нашей реплики, пауза повтора сегмента, пауза после любого нашего сообщения (поле расписания «Не писать, если писали за N дней»), подача этим расписанием ушла или ещё идёт), paid — купил продукт после регистрации, по которой попал в сегмент, withManager — с ним работает менеджер (действующая заявка на продукт после этой регистрации, операторский статус или передача диалога в отдел моложе трёх суток), blocked — бан, удалённая карточка или действующий мьют, activeSeries — идёт другая вебинарная серия, retryPause — прошлая попытка отправки этим расписанием не удалась, ждём паузу, overCap — не поместились в дневной потолок, higherPriority — уйдут в сегмент с большим приоритетом, otherPlatform — карточка на другой площадке школы, и эта авторассылка её не забирает (передача выключена, там ещё идёт серия эфиров или её дата неизвестна, карточек несколько, перенос не удался или уже увёз человека отсюда; у расчётов и прогонов до 22.09.2026 этой группы нет — эти люди в other), other — прочее (предохранитель не смог проверить заявки и оплаты, карточки на этой площадке нет, повтору не нашлось прошлого касания, непригодный номер человека); у прогонов до 16.09.2026 к новым группам добавляются прежние held (купил, менеджер, бан или мьют) и pause (пауза), только ненулевые; alreadyWritten у прогонов не бывает — называй их так и не складывай с новыми); unrecorded — людей пула без записанной причины (должно быть 0); taken — взято запуском (люди журнала подач, с этого числа начинается отправка); selection=null — пул не измерен. scheduleRun.facts — отправка по людям: sent — отправлено, noMessenger — карточку не завели или ни один канал не доходит, pending — ещё отправляется, notSent — не ушло по другой причине, notSentByReason — те же notSent по причинам: один исход на человека, сумма равна notSent; ключ — исход отказа отправки (SKIPPED_BOT_SILENCED — бот выключен у контакта, SKIPPED_ALREADY_IN_DIALOG — разговор уже идёт, SKIPPED_UNRESOLVED_VARS — переменные текста без значения и соседи, те же коды, что в outcomes ручной отправки), без следа отправки — исход журнала подач (UNRESOLVED_AFTER_BATCH — причина не записана: прогоны до 16.09.2026, RELEASED_LAUNCH_FAILED — запуск сорвался); человеку называй причины словами, а не кодами; facts=null — людей прогон не брал. excludedBy и selectedCount — сырые счётчики прогона: selectedCount после подачи уже без людей, которых импорт не завёл, — «взято» по нему не называй; причины источника в excludedBy (unverifiable, lowCoverageSkipped и соседи) отсеяны ДО пула — с пулом их не складывай и человеку не показывай как отсев пула.',
512
- '⚠️⚠️ Ручная отправка ПАКЕТОМ — ОДНА строка с batch: id строки = batch.batchId, batch.blocks — блоки «сегмент × продукт × воронка» в порядке исполнения (status=pending и importJobId=null — блок ещё не стартовал). totalContacts — план всего пакета, sentCount/failedCount/outcomes — суммы по блокам, tracedOutcomes у строки пакета ВСЕГДА null: поимённый разбор — у блоков, по их importJobId. Пакет из одного блока отдаётся обычной строкой задачи (batch=null). Остановить пакет — besales_getcourse_cold_welcome_batch_cancel с batch.batchId, а не отменой задачи по id строки.',
513
- '⚠️ Автоприём регистраций (приветствие по вебхуку регистрации) — ОДНА строка на день с registrationDay.day (дата МСК): sentCount = totalContacts = отправлено за день (списка «взяли» у потока нет), outcomes и tracedOutcomes null, status=running значит «день ещё идёт», а не «идёт прогон». Останавливать у такой строки нечего.',
514
- '⚠️ scheduleRun.reason приходит и у прогона, доставившего ЧАСТЬ подачи: пакет пишет ошибку вместе с числом отправленных. status=failed у такой строки означает «дочерний пакет вернул не COMPLETED», а не «не ушло ничего».',
515
- 'schedule_id — только прогоны этого расписания; ручные отправки, волны и автоприём регистраций при этом не возвращаются вовсе, они расписанием не запускались.',
516
- 'status=failed у cold_welcome означает и обрыв процесса: прогон не финализировал себя. outcomes — разбивка «почему не ушло».',
517
- 'В outcomes чинится оператором: SKIPPED_NO_ANCHOR (нет даты эфира), SKIPPED_TEXT_TOO_LONG (текст длиннее лимита после подстановки), SKIPPED_UNRESOLVED_VARS (переменные текста без значения), SKIPPED_BOT_SILENCED (бот выключен у контакта), SKIPPED_NOT_IN_BASE (контакта нет в базе — сначала импорт файла). Отметка приветствия у них не сгорала — после починки повторный запуск их доберёт.',
518
- 'SKIPPED_RESEND_PAUSE — оператором НЕ чинится: этим сегментом человеку уже писали, после новой регистрации расписание повторяет ему сегмент, но пауза повтора после последней отправки ещё не прошла (чаще всего сегмент успел уйти ему соседним запуском). Бывает только у прогона расписания; человек вернётся в пул сам.',
519
- '⚠️⚠️ sentWithoutChannelEvidence — сколько из sentCount ушло ВСЛЕПУЮ: канал взят резервным транспортом площадки, а не доказан привязками контакта или его ответом. Это ПОДМНОЖЕСТВО отправленных, а не отдельный исход: сообщение ушло, повторять его нельзя. Такие контакты с большой вероятностью получили сообщение в личный кабинет сайта, а не в мессенджер, — по outcomes они неотличимы от дошедших. null (а не 0) означает «счётчик не считался»: волна либо запуск до его появления.',
520
- '⚠️ SKIPPED_ANCHOR_IN_PAST — ИСКЛЮЧЕНИЕ из строки выше: дата эфира у контакта ЕСТЬ и устарела (эфир перенесли). Повторный запуск даст ровно тот же исход, сколько ни запускай: автоматический добор якоря — set-if-absent и к заполненному значению не возвращается. Лечится ТОЛЬКО явным перечитыванием якорей из запинённой сделки (POST /api/chat-users/getcourse-anchor-refresh), после которого контакт доберётся обычным запуском.',
810
+ 'История запусков ПЯТИ источников (origin): MANUAL — ручная отправка «Первого сообщения», WAVE — волна по файлу, SCHEDULE — прогон авторассылки, REGISTRATION — автоприём регистраций (строка на день), CAMPAIGN — прогон движка рассылок. У четырёх прежних источников mechanism: COLD_WELCOME — пишем первыми сами, WAVE — отправляет процесс школы. status: RUNNING, COMPLETED, CANCELLED, FAILED; человеку называй подписи (statusLabel, originLabel, label у исходов), а не коды.',
811
+ 'У CAMPAIGN имя, канал, статус словами и автор — в campaignRecord (kind=RUN); card содержит адрес существующей карточки API. Инструмента открытия этой карточки в мосту нет. status и statusLabel совпадают с записью, включая PAUSED, ARCHIVED и пометку stopped; totalContacts — recipients, sentCount — переданные провайдеру строки, включая частичные, failedCount — ошибки/неизвестные без передачи. processed не означает отправлено. stop=null: общую отмену BATCH/JOB не вызывай. allowedTransitions называет допустимые состояния, а не право читателя управлять рассылкой. schedule_id или mailing_variant_id исключает CAMPAIGN. hasMore=true — продолжи с nextCursor без преобразования; ошибку курсора покажи, первую страницу вместо неё не запрашивай.',
812
+ '⚠️⚠️ Прогон авторассылки (origin=SCHEDULE) называй так же, как вкладки «История» и «Пул сегодня»: «взято · отправлено» из scheduleRun.headline (taken — люди журнала подач, sent — люди с доказанной отправкой). poolMeasured=false — «счётчиков отбора нет», taken=null, и это НЕ ноль: прогон ещё не обращался к источнику. Пул окна свежести (scheduleRun.poolSize) — регистрации всего окна, почти всем из которых уже писали: людьми дня его не называй. У прогона totalContacts — взято (люди журнала подач), sentCount — счётчик попыток прогона (доставленные и занятые без доказательства), failedCount всегда 0, outcomes — null.',
813
+ 'scheduleRun.facts — отправка по людям: sent — отправлено, noMessenger — карточку не завели или ни один выбранный канал не доходит, pending — ещё отправляется, notSent — не ушло по другой причине, notSentByReason — [{code, label, count}], один исход на человека, сумма равна notSent (код — исход отказа отправки, без следа отправки — исход журнала подач). facts=null — «ещё неизвестно»: прогон не завершён и никого не взял; у завершённого прогона без взятых — нули, это доказанный ноль.',
814
+ 'scheduleRun.dayPlan — план по людям из утреннего расчёта «Пула сегодня» ЭТОГО сегмента на день прогона: breakdown — те же числа, что вкладка «Пул» показывает у сегмента (registrations, people, excluded по группам, selected, carriedOver), planned — «Уйдёт» по плану, delta — «Взято» минус план (плюс — добавились к запуску, минус — отпали); dayPlan=null — плана нет, а не нули. scheduleRun.selection — отбор прогона в группах «Пула» (collapsed — несколько регистраций одного человека; excluded — noMessenger, inContact, paid, withManager, blocked, activeSeries, retryPause, overCap, higherPriority, otherPlatform, other, у прогонов до 16.09.2026 ещё held и pause; unrecorded — без записанной причины, должно быть 0). excludedBy и selectedCount — сырые счётчики: selectedCount «взято» не называй, отсев источника фактов в excludedBy (unverifiable и соседи) — до пула, с пулом его не складывай.',
815
+ '⚠️ scheduleRun.reason — причина закрытия или ожидания словами; приходит и у прогона, доставившего ЧАСТЬ подачи. reasonDetail — текст, который записал сам прогон (у отсрочки из-за источника фактов — подробности). status=RUNNING у прогона, ждущего ресурса, — период не сгорел, проход повторит его сам. guardUnchecked — предохранитель в этот день не проверил часть отобранных (causeLabel — почему): отсев доставки по серии, заявке и оплате в такой день выше обычного.',
816
+ '⚠️⚠️ Ручная отправка пакетом — ОДНА строка с batch: id строки = batch.batchId, batch.blocks — блоки в порядке исполнения (status=PENDING и importJobId=null — блок ещё не стартовал); totalContacts — план всего пакета, суммы — по всем блокам, mailingVariants — уникальные варианты начавшихся блоков; смешанный пакет принадлежит каждому из них, блоки содержат mailingVariantId/Name. Фильтр варианта сохраняет весь пакет, его время и суммы; сводка варианта считает отправку только его блоков, не начавшиеся запуска не дают, tracedOutcomes у строки пакета null: поимённый разбор — у блоков, по их importJobId. Пакет из одного блока — обычная строка задачи (batch=null). Остановить строку — по полю stop: kind=BATCH — besales_getcourse_cold_welcome_batch_cancel с targetId, kind=JOB — besales_getcourse_cold_welcome_cancel с targetId; stop=null — остановки нет (прогон, волна, день автоприёма, завершённая строка).',
817
+ SCHEDULE_POOL_GROUPS_HINT,
818
+ SCHEDULE_EXCLUSION_HINT,
819
+ 'Автоприём регистраций — строка на день registrationDay.day (московский день): sentCount = totalContacts = отправлено за день (списка «взяли» у потока нет), status=RUNNING значит «день ещё идёт», а не «идёт прогон».',
820
+ 'schedule_id — только прогоны этого расписания: ручные отправки, волны и автоприём регистраций не возвращаются вовсе. mailing_variant_id у прогона — рассылка его СНИМКА: сменили рассылку у расписания — вчерашние прогоны остались у прежней. hasMore=true — строк больше, чем limit: сузь выборку фильтром.',
821
+ 'outcomes — [{code, label, count}] ручной отправки: почему человек остался без сообщения. Чинится оператором SKIPPED_NO_ANCHOR, SKIPPED_TEXT_TOO_LONG, SKIPPED_UNRESOLVED_VARS, SKIPPED_BOT_SILENCED, SKIPPED_NOT_IN_BASE — после починки повторный запуск их доберёт; SKIPPED_ANCHOR_IN_PAST — только перечитыванием якорей из сделки (besales_getcourse_anchor_refresh_start); SKIPPED_RESEND_PAUSE оператором не чинится: человек вернётся в пул сам. sentWithoutChannelEvidence — сколько из отправленных ушло вслепую (подмножество отправленных, повторять нельзя); null — не считалось.',
521
822
  ]),
522
823
  }),
523
824
  ];