CRM-действия роботов: контракт CRM ↔ robots-clj | Napulse

CRM-действия роботов: контракт CRM ↔ robots-clj

Внутренний документ для разработчиков. В меню справки его нет. Это контракт между CRM (Napulse) и сервисом роботов robots-clj для четырёх действий робота:

  • создать задачу;
  • перевести сделку на этап;
  • назначить ответственного;
  • добавить заметку.

Источник: audits/mcp-research-2026-10-17/DESIGN-phases-2-4.md, разделы 4.C (сторона CRM) и 4.D (сторона robots-clj).

Состояние

Сторона Что есть
CRM Готово в ветке feat/mcp-p3-robotactions-20261017: ручки POST /api/internal/robots/crm-actions и GET /api/internal/robots/crm-catalog, таблица robot_crm_action_runs (миграция 0258), журнал source = robot. Код: packages/root/server/internal-robots-crm-actions-route.ts, services/crm-actions/robot-*.ts.
robots-clj Не сделано. Делает отдельная сессия после слияния веток роботов, которые правят те же файлы: conditions, save-conflict, scenario-semantics, autodialog-followup. Что сделать — раздел «Что сделать в robots-clj» ниже.

Кто действует и в каких правах

Робот действует в пределах прав своего автора: это сотрудник из robots.created_by. Права проверяются при каждом вызове, по текущим правам автора в проекте. Права того, кто правил робота после автора, CRM не проверяет. Это открытый риск, см. раздел «Известный риск: робота правил не автор».

Действие Нужное право автора (то же правило, что на экране CRM)
create_task управление задачами (tasks.manage, management.manage) или задачами клиентов (clients.manage, chats.manage)
set_deal_stage ручное изменение сделок (deals.manage, clients.manage, management.manage, tech_block.manage)
set_responsible роль проекта «администратор» (права уровня управления) или владелец проекта
add_note любая видимость диалогов клиентов

Отказы:

  • Автор не записан (created_by IS NULL, роботы до 13.09.2026) или записан не id сотрудника CRM → 403 robot_author_unknown. Конструктор должен показать подсказку: «CRM-действия недоступны: у робота не записан автор».
  • Автор больше не активный сотрудник проекта (удалён, заблокирован, вышел из проекта) → 403 author_lacks_permission. Роботы ушедшего сотрудника его права не сохраняют.

Технический автор записи — служебная учётная запись проекта. Исполнителем задачи или сделки она никогда не становится.

В CRM действие подписано Робот «<имя робота>». Подпись видна:

  • в ленте клиента;
  • в авторе заметки;
  • в истории сделки;
  • в событии смены ответственного.

Выключатели MCP (на проект и на платформу) CRM-действия роботов не останавливают.

Известный риск: робота правил не автор

Статус: открыт, нужно решение владельца до раздела 4.D. Сейчас риск не действует: robots-clj эту ручку не вызывает, CRM-узлов в конструкторе нет.

В чём риск. CRM проверяет права только у автора робота (robots.created_by). Кто последним менял сценарий, CRM не знает.

При этом в robots-clj:

  • сотрудник с правом редактировать роботов «все» может менять чужих роботов (src/robots_clj/robot_access.clj, permitted?);
  • created_by при правке не меняется (src/robots_clj/db/robots.clj, update-robot!).

Когда появятся CRM-узлы (4.D), такой сотрудник сможет добавить CRM-действие в робота администратора или владельца. CRM проверит права автора и выполнит действие с подписью «Робот «<имя>»». Так сотрудник получит то, что ему самому не разрешено:

  • назначить ответственного (можно только администратору или владельцу);
  • перевести сделку на этап (нужно право на ручные изменения сделок);
  • оставить заметку в диалоге, которого он не видит.

Это касается и сотрудника технического блока, которому CRM не показывает карточки клиентов.

Почему не закрыто на стороне CRM. В запросе robots-clj нет того, кто менял робота. Восстановить это по базе роботов ненадёжно:

  • версии (robot_versions.changed_by) пишутся при сохранении, автосохранении и активации;
  • но импорт роботов (POST …/robots/import) перезаписывает сценарий существующего робота через update-robot! и не записывает, кто это сделал.

Проверка CRM по версиям обходилась бы импортом.

Варианты (выбирает владелец):

Вариант Что меняется Минус
А. Права автора и последнего редактора (рекомендуется) robots-clj записывает, кто последним менял сценарий, на любом пути, включая импорт. Это id сотрудника передаётся в запросе как обязательное поле editorUserId. CRM пропускает действие, только если право есть и у автора, и у редактора. Если сценарий поправит сотрудник без нужного права, даже в другом узле, CRM-действия этого робота начнут получать 403 author_lacks_permission. Так будет, пока робота не пересохранит сотрудник с правом.
Б. Запрет в конструкторе CRM-узлы добавляет и меняет только автор робота или сотрудник, у которого есть право этого действия. robots-clj спрашивает права у CRM. Проверка нужна на каждом пути записи сценария (сохранение, автосохранение, импорт, активация версии). Пропущенный путь снова открывает риск.
В. Принять риск Ничего, только запись в трекере аудита безопасности. Риск остаётся.

Рекомендация: не выкатывать CRM-узлы в robots-clj (4.D), пока владелец не выбрал вариант.

Авторизация

  • Заголовок x-robots-service-token. Его значение равно ROBOTS_SERVICE_TOKEN на стороне CRM и CRM_ROBOTS_SERVICE_TOKEN на стороне robots-clj.
  • Если ROBOTS_SERVICE_TOKEN в CRM пуст, обе ручки отвечают 503 service_auth_not_configured. Пустой секрет не открывает доступ.
  • Неверный токен или токена нет → 401 unauthorized. Токен сравнивается за постоянное время.
  • Адрес CRM для robots-clj — CRM_INTERNAL_BASE_URL, как у остальных внутренних ручек.

Все ответы приходят с Cache-Control: no-store, тело — JSON.

POST /api/internal/robots/crm-actions

Тело запроса

Объект строгий: лишний ключ → 400 invalid_body. Необязательные поля можно передавать как null (так Clojure кодирует nil).

Поле Тип Обязательно Смысл
action create_task | set_deal_stage | set_responsible | add_note да действие
projectId uuid да (:project_id robot); CRM сверяет его с проектом робота
robotUuid uuid да uuid робота в robots-clj
robotId строка ≤255 нет текстовый id робота, только для журнала
stepId строка ≤255 да id узла сценария
executionId строка ≤255 нет id прохождения (run)
platform vk | telegram да platform-contract-value канала
sourceId строка ≤255 да channel-source-id канала (tgbot:…, tguser:…, vk:…)
platformUserId строка ≤255 да id клиента в мессенджере
idempotencyKey строка 1..255 да см. «Идемпотентность»
params объект да параметры действия, см. ниже; не подходят к действию → 400 invalid_params

Проект и диалог CRM определяет сама:

  1. Робот берётся по robotUuid, и его project_id должен совпасть с projectId.
  2. Клиент ищется по (platform, platformUserId, projectId).
  3. Диалог — открытый диалог этого клиента с sourceId, в этом же проекте. Это тот же поиск, что у внутренних ручек сообщений роботов.

Записать что-то в чужой проект или в чужой диалог через тело нельзя.

Параметры действий

Тексты (title, description, text) robots-clj передаёт уже после подстановки переменных (resolve-placeholders).

create_task

Поле Тип Обязательно Смысл
title строка 1..500 (после trim) да заголовок
description строка ≤50000 нет описание
priority low | medium | high | urgent нет по умолчанию medium
assignee {mode, userId?} да исполнитель
dueInMinutes целое 1..525600 нет срок через N минут от вызова
dueAt дата ISO 8601 с часовым поясом нет точный срок; вместе с dueInMinutes нельзя

Исполнитель задаётся полем assignee.mode:

  • conversation_manager — ответственный за диалог. Если ответственного нет или он больше не может получать задачи, задача остаётся без исполнителя.
  • user — сотрудник userId. Он должен быть активным сотрудником проекта, иначе 400 invalid_assignee.
  • none — без исполнителя.

userId передаётся только с mode = user.

Задача привязана к диалогу робота. В ленту клиента пишется событие task_created с source: "robot".

set_deal_stage

Поле Тип Обязательно Смысл
pipelineId uuid да воронка проекта, иначе 404 pipeline_not_found
stageId uuid да этап этой воронки, иначе 404 stage_not_found
createIfMissing boolean нет по умолчанию false
dealTitle строка 1..500 нет название новой сделки

Берётся последняя открытая сделка клиента в этой воронке и этом диалоге, и она переводится на этап. В историю сделки пишется событие, подписанное роботом.

Если такой сделки нет:

  • при createIfMissing = false → 404 deal_not_found;
  • при createIfMissing = true создаётся новая сделка: линия диалога, исполнитель — ответственный за диалог (если он ещё активный сотрудник), название dealTitle или «Сделка N» по нумерации CRM. Срабатывает автоматика «сделка → статус диалога».

Если сделка уже стоит на этом этапе, ответ changed: false.

set_responsible

Поле Тип Обязательно Смысл
mode user | unassign да назначить сотрудника или снять ответственного
userId uuid при mode = user сотрудник проекта с ролью «администратор» или «менеджер», активный, иначе 400 invalid_responsible

Распределения «по правилам» нет. Открытые сделки диалога переходят к новому ответственному, как при смене на экране. В ленту клиента пишется manager_reassigned.

add_note

Поле Тип Обязательно Смысл
text строка 1..10000 (после trim) да текст заметки

Заметка появляется в диалоге с автором Робот «<имя>». В ленту пишется note_added, открытые экраны обновляются.

Ответы

Успех: 200 и тело

{ "ok": true, "action": "create_task", "result": { "...": "..." } }
        

Поле result по действиям:

Действие result
create_task {taskId, conversationId, assignedTo, priority, dueDate}
set_deal_stage {dealId, pipelineId, stageId, created, changed}
set_responsible {conversationId, assignedManagerId, changed}
add_note {noteId, conversationId}

Отказ — то же тело с ok: false: { "ok": false, "action": "...", "error": "<код>" }.

HTTP error Когда
400 invalid_body битый JSON, не объект, лишний ключ, неверный формат поля
400 invalid_params params не подходят к действию
400 invalid_assignee исполнитель задачи не активный сотрудник проекта
400 invalid_responsible ответственный не активный сотрудник проекта с ролью «администратор»/«менеджер»
401 unauthorized нет токена или он неверный
403 robot_project_mismatch робот из другого проекта
403 robot_author_unknown у робота не записан автор
403 author_lacks_permission у автора нет нужного права или он больше не сотрудник проекта
404 robot_not_found робота с таким uuid нет (в том числе если база роботов не видна CRM)
404 contact_not_found клиента с (platform, platformUserId) нет в проекте
404 conversation_not_found у клиента нет открытого диалога с этим sourceId в проекте
404 pipeline_not_found, stage_not_found, deal_not_found см. set_deal_stage
405 method_not_allowed не POST
409 idempotency_key_conflict ключ уже использован другим действием, роботом или проектом
409 idempotency_in_progress действие с этим ключом ещё выполняется (или прервалось)
500 internal внутренняя ошибка
503 service_auth_not_configured в CRM не задан ROBOTS_SERVICE_TOKEN
503 robots_source_unavailable CRM не может прочитать базу роботов (нет соединения, таймаут 5 с). Ничего не записано и не попало в журнал

Идемпотентность

  • Формат ключа: "<robotUuid>:<executionId или run-id>:<stepId>:<action>". Один шаг одного прохождения — одно действие.
  • Повтор с тем же ключом возвращает первый ответ с добавленным deduplicated: true и ничего не делает повторно. Это касается и отказа, полученного на самом действии (например, 400 invalid_assignee). Повтор отвечается раньше всех остальных проверок.
  • Отказы до действия не запоминаются: неизвестный робот, чужой проект, нет клиента или диалога, нет права автора. Записи не было, поэтому повтор получит тот же отказ заново.
  • Ключ привязан к проекту, роботу и действию. Тот же ключ с другими значениями → 409 idempotency_key_conflict.
  • Конкурирующий повтор во время выполнения → 409 idempotency_in_progress. Действие при этом могло выполниться. Повторять его не нужно.
  • Срок хранения записей — 30 дней.
  • Повтор внутри CRM: CRM сама пишущий запрос не повторяет.

Журнал

Каждый вызов, прошедший проверку робота, пишется в журнал MCP проекта (mcp_audit_log):

  • source = robot;
  • user_id — автор робота (у робота без автора пусто);
  • tool = robot.<action>;
  • object_type = conversation, object_id — id диалога (если он найден);
  • ok и код ошибки;
  • длительность;
  • args: {robotUuid, robotId?, stepId, executionId?, params}.

Тексты клиентов в журнал не попадают. title, description и dealTitle хранятся как длина и отпечаток SHA-256, text заметки — так же, через общее редактирование журнала. Повторы по ключу в журнал не пишутся. Журнал хранится 180 дней.

Запись журнала делается после действия, и её сбой не превращает выполненное действие в ошибку: сбой уходит в лог сервера.

GET /api/internal/robots/crm-catalog?projectId=<uuid>

Справочник для конструктора: воронки с этапами и сотрудники, которых робот может назначить. Почт в ответе нет.

{
          "ok": true,
          "pipelines": [{ "id": "…", "name": "Продажи", "stages": [{ "id": "…", "name": "Новый лид" }] }],
          "users": [{ "id": "…", "displayName": "Мария Менеджер" }]
        }
        
  • users — активные сотрудники проекта с ролью «администратор» или «менеджер», до 500 человек. Это тот же список, что «кому можно передать диалог» в карточке клиента. displayName может быть null, если в профиле нет имени.
  • Ровно один корректный projectId, иначе 400 invalid_query. Метод только GET, иначе 405. Токен — как выше.
  • Ручка доверяет вызывающему по токену. Права сотрудника, открывшего конструктор, проверяет robots-clj до запроса в CRM (см. п. 6 ниже).

Что сделать в robots-clj (раздел 4.D)

Делать только после слияния веток роботов (conditions, save-conflict, scenario-semantics, autodialog-followup) и после решения владельца по разделу «Известный риск: робота правил не автор». При варианте А или Б к шагам ниже добавляется его доработка: в robots-clj и, для варианта А, поле editorUserId в контракте CRM.

  1. Новый ns robots-clj.robots.crm-actions по образцу robots/sheets_write.clj (если его ещё нет в main — по образцу bulk_access.clj и вызовов подписных страниц в actions.clj):

    • service-config читает CRM_INTERNAL_BASE_URL и CRM_ROBOTS_SERVICE_TOKEN; без них возвращает {:status :error :message "Missing …"};
    • execute! [project-id robot channel platform user-id action params exec-context] делает POST на /api/internal/robots/crm-actions с заголовком x-robots-service-token; таймауты: соединение 5000 мс, сокет 15000 мс; :throw-exceptions false;
    • повтор — не больше одного раза и только при ошибке соединения, с тем же idempotencyKey. Ответы 4xx/5xx не повторять;
    • возврат {:status :ok :result …} или {:status :error :message <error> :code <http>}.
  2. schemas.clj. В ActionType добавить crm_create_task, crm_set_deal_stage, crm_set_responsible, crm_add_note. В схему узла — необязательный ключ :crm.

  3. engine.clj normalize-intent:

    • crm_create_task → {:type :crm/create_task :title :description :priority :assignee :dueInMinutes};
    • crm_set_deal_stage → {:type :crm/set_deal_stage :pipelineId :stageId :createIfMissing :dealTitle};
    • crm_set_responsible → {:type :crm/set_responsible :mode :userId};
    • crm_add_note → {:type :crm/add_note :text};
    • в blocks/action/core.clj — записи с :platforms #{:vk :tg :all}.

    Ключ :type в params не передавать: тело строгое, лишний ключ даст 400 invalid_params.

  4. actions.clj execute-action (ветка до запасного варианта):

    • title, description и text прогоняются через resolve-placeholders;
    • sourceId берётся из channel-source-id, platform — из platform-contract-value, projectId — из (:project_id robot);
    • ошибку CRM не бросать исключением, а возвращать {:status :error}. Сценарий идёт дальше, как у остальных внешних действий.
  5. channel_capabilities.clj: новые типы добавить только в known-actions, не в platform_actions.

  6. routes.clj: прокси GET /api/projects/:uuid/crm-catalog → CRM GET /api/internal/robots/crm-catalog?projectId=:uuid, по образцу /subscriber-pages. Перед прокси проверить контекст сотрудника (x-session-user-*): проект из пути равен x-session-user-project-id, и у сотрудника есть право редактировать роботов.

  7. Конструктор (frontend/src/components/robot-builder/operation-catalog.ts, types.ts, NodeSettingsModal.tsx):

    • выпадающие списки воронки, этапа и сотрудника из crm-catalog;
    • для робота без автора (created_by пуст) — подсказка «CRM-действия недоступны: у робота не записан автор».
  8. Тесты по образцу test/robots_clj/robots/sheets_write_test.clj:

    • нет конфигурации → :error;
    • заголовок и тело запроса;
    • повтор только при ошибке соединения и с тем же ключом;
    • 4xx возвращается как {:status :error :code …};
    • плейсхолдеры подставлены до отправки.

Не проверено (проверить до выката)

  • Задан ли на проде ROBOTS_SERVICE_TOKEN в CRM и CRM_ROBOTS_SERVICE_TOKEN в robots-clj, и одинаковые ли они.
  • Откуда CRM читает таблицу robots: ANALYTICS_ROBOTS_DATABASE_URL → ANALYTICS_SOURCE_DATABASE_URL (реплика) → DATABASE_URL, схема ANALYTICS_ROBOTS_DB_SCHEMA (по умолчанию robots_clj). Если это реплика, отставание даст ложный 404 robot_not_found для робота, созданного секунды назад.
  • Сколько роботов на проде без created_by: для них CRM-действия будут отказывать (robot_author_unknown).
Следующая