Двусторонний мост между групповыми чатами мессенджера MAX (
dev.max.ru) и Открытыми линиями Битрикс24.
Штатный MAX-коннектор Битрикс24 не умеет работать с групповыми чатами. Этот коннектор закрывает пробел: один MAX-чат → одна сессия Open Lines, каждый участник группы опознаётся как отдельный отправитель внутри одной сессии. Сообщения ходят в обе стороны с автоматической подписью оператора при ответах в группу.
Single-tenant: один портал Б24 и один бот MAX на установку.
flowchart LR
subgraph MAX
G["Группа MAX<br/>участники + бот-админ"]
end
subgraph Bridge["Мост (этот репозиторий)"]
WM["POST /webhook/max"]
WB["POST /webhook/b24"]
M["messagingService<br/>идемпотентность, очередь по чату"]
DB[("PostgreSQL<br/>привязки, участники, message_log")]
end
subgraph B24["Битрикс24"]
OL["Открытая линия<br/>группа = одна сессия"]
OP(["Оператор"])
end
G -- "webhook: сообщения, правки, удаления" --> WM --> M
M -- "imconnector.send / update / delete.messages" --> OL
OP --- OL
OL -- "OnImConnectorMessageAdd / Update / Delete" --> WB --> M
M -- "Bot API: POST / PUT / DELETE /messages" --> G
M <--> DB
Бот MAX — администратор группы: только так он получает все её сообщения. Первое сообщение из новой группы само создаёт привязку «группа ↔ чат Открытой линии», настраивать группы вручную не нужно.
- Входящие — приём webhook'ов MAX Bot API о событиях в подключённых группах, идентификация каждого автора внутри одной сессии Open Lines (
imconnector.register(CHAT_GROUP=true)). Только группы: личка с ботом не открывает сессию ОЛ (автор получает одну подсказку «работаю только в группах»), посты каналов пропускаются. - Исходящие — обработка
OnImConnectorMessageAddот Б24, отправка ответа оператора обратно в MAX-группу с автоматической подписью (NEED_SIGNATURE=true). - Очередь исходящих и безопасная остановка — отправка, правка и удаление одного чата, уже пришедшие в мост, идут в MAX строго по очереди: удаление и правка, пришедшие, пока мост заливает файл в MAX, ждут конца заливки (событие о файле Б24 присылает только после его полной загрузки в Битрикс24, поэтому текст, отправленный во время такой загрузки, придёт раньше файла). Ответ Б24 — после доставки. При деплое и
docker stopмост не закрывает порт, пока не закончит передачи (до 170 с;stop_grace_period180 с); передачу, всё же прерванную рестартом, оператор видит системным сообщением в ОЛ «мост перезапускался». - Передача файлов — фото, видео, документы и стикеры ходят в обе стороны; входящие голосовые (audio →
.ogg, встроенный аудио-плеер в Open Lines) — MAX → Open Lines. Inbound — черезmessage.files(Б24 качает по url сам); outbound — через MAX upload-флоу потоком через диск, по файлу за раз, с лимитомMEDIA_MAX_BYTES(256 МиБ); недоставленный файл оператор видит системным сообщением в ОЛ. Картинку, которую MAX не принимает как изображение (SVG, HEIC, сторона больше 7680 px, отказ MAX при заливке), мост отправляет файлом. - Reply/цитирование — двусторонний мост «ответ на сообщение». MAX→OL: ответ участника превращается в визуальную
>>-цитату в тексте OL (без БД и REST). OL→MAX: reply оператора становится нативным кликабельным reply в MAX (доборREPLY_IDчерезim.dialog.messages.search, отключаемо). Best-effort: сбой не валит доставку. - Редактирование сообщений — двусторонний мост правки текста. MAX→OL:
message_edited(тот жеmid) обновляет сообщение в OL черезimconnector.update.messages+ маркерИзменено. OL→MAX:OnImConnectorMessageUpdateправит сообщение в MAX черезPUT /messages(MAX сам ставит «Ред.»); правку, которая не дошла до MAX, оператор видит системным сообщением в ОЛ. Идемпотентность по синтетическому ключу без миграции БД (MAX→OLedit:<mid>:<ts>; OL→MAXedit:<b24MsgId>:<хеш текста>— вOnImConnectorMessageUpdateнетmessage.date); эхо-защита Б24 исключает петли. Правка медиа — вне скоупа. - Удаление сообщений — двусторонний мост удаления. MAX→OL:
message_removed(тот жеmid) удаляет сообщение в OL черезimconnector.delete.messages(с ретраями). OL→MAX:OnImConnectorMessageDeleteудаляет сообщение в MAX черезDELETE /messages; если удалить не вышло (сбой MAX, отказ MAX —max-window, удалено не всё), оператор видит системное сообщение в ОЛ. Идемпотентность по синтетическому ключуdelete:<id>без миграции БД; эхо-защита by design на обеих платформах исключает петли. Работает и для медиа (удаление по внешнему id). - Название и иконка группы в чате ОЛ — новый чат Open Lines получает название и иконку MAX-группы, а не имя и фото первого автора. Первым в чат пишет служебный пользователь «группа» (Б24 делает его фото аватаром чата), точное название ставит
imconnector.chat.name.set. Переименование группы в MAX (chat_title_changed) доходит до ОЛ; при закрытой сессии — на следующем сообщении. Ссылку на группу в MAX оператор получает системным сообщением в чате ОЛ — группы клиентов приватные, по названию их не найти: новой группе — сразу после интро, подключённой раньше — один раз на следующем сообщении, повтор — только если MAX перевыпустил ссылку. - Группа — «Клиент» каждой сессии ОЛ — в журнале диалогов и в контакт-центре видна группа, а не случайный участник. После закрытия диалога (
OnImConnectorDialogFinish) следующую сессию открывает служебное «Новое обращение из группы «…» в MAX» от той же «группы». Если событие закрытия потеряно, а группу переименовали при закрытом диалоге, опенер всё равно уйдёт: закрытие мост узнаёт по ответуCHAT_RENAMING_FAILEDна переименование чата ОЛ и проверяет черезimopenlines.dialog.get. - Уведомления о составе группы — при вступлении/выходе участника (
user_added/user_removed) оператору в OL-чате показывается системное сообщение (im.message.add,SYSTEM='Y'); сессию не открывает, клиентам в MAX не уходит. Best-effort, не валит вебхук. - CRM-привязка — выполняется оператором вручную в нативном UI Открытых линий; коннектор её не автоматизирует.
- Идемпотентность — дубли входящих (по
body.midMAX) и исходящих (по message id Б24) не создают повторных записей; журнал вmessage_log. Повтор исходящего события, пока первое ещё выполняется, отсекает строка заданияjob:. - Health-check — runtime-проверка admin-прав бота в группе (
read_all_messages) с предупреждением в чат. - Безопасность webhook'ов —
X-Max-Bot-Api-Secretдля MAX,application_tokenдля событий Б24. - Удаление приложения Б24 —
ONAPPUNINSTALLсбрасывает токены портала, приCLEAN=1удаляет привязки, участников и журнал; до переустановки/webhook/b24отвечает503 B24_APP_UNINSTALLED, новые группы не подключаются.
| Слой | Технология |
|---|---|
| Backend | Node.js 24 (ESM) + Express 5 |
| База данных | PostgreSQL (pg) |
| Проверка входящих | plain-secret X-Max-Bot-Api-Secret (MAX), application_token + member_id (Б24), install-callback — обмен refresh_token |
| Внешние API | MAX Bot API, Bitrix24 REST (imconnector.*, imopenlines.*, im.*, OAuth) |
| Контейнеризация | Docker + Docker Compose |
Backend организован по паттерну Structured Modules (Vertical Slices by Entity) — адаптеры max/ и b24/ общаются только через координатор messaging/, где сосредоточены идемпотентность и audit-лог. Обзор слоёв и модулей — в docs/backend/architecture.md.
Шаги ниже поднимают мост в dev-режиме (nodemon, код смонтирован в контейнер) — так проще всего попробовать. Боевой режим за общим Caddy с Let's Encrypt, деплой и бэкапы — docs/deploy/vps-prod.md.
- Портал Битрикс24 с правами администратора: нужны локальное приложение и Контакт-центр. REST API Б24 работает только на коммерческих тарифах.
- Бот MAX и его токен (шаг 1).
- Сервер с Docker (плагин
docker compose) и домен с валидным HTTPS на порту 443: MAX шлёт webhook только туда; сертификат — от доверенного центра (Let's Encrypt подходит) с полной цепочкой, самоподписанные не принимаются. Для разработки — любой HTTPS-туннель с валидным сертификатом до127.0.0.1:8000. Хост лучше держать в РФ: из-за рубежа порталы Б24 отвечают нестабильно (почему). - Node.js 24 и pnpm 9 — только для тестов и запуска скриптов на хосте; сам мост работает в Docker.
Боты MAX создаются на платформе MAX для партнёров (сверено с dev.max.ru 2026-10-22):
- Зарегистрируйтесь и пройдите верификацию профиля — она доступна юрлицам, ИП и самозанятым, резидентам РФ (подключение). Без верифицированного профиля бота не создать.
- Чат-боты → Создать: логотип, название, описание (создание бота). Бот уходит на модерацию — до 48 часов по рабочим дням.
- После модерации: Чат-боты → бот → ⋮ → Настройки — скопируйте «Токен доступа» (управление ботом). Там же, в разделе «Приватность», разрешите добавлять бота в групповые чаты — по умолчанию это запрещено.
- Придумайте секрет вебхука:
openssl rand -hex 32(MAX принимает 5–256 символов из букв, цифр,-и_).
Бот пока не добавляйте в группу — это шаг 6.
git clone https://github.com/maxadrom/MAXTOB24.git
cd MAXTOB24
cp .env.example .envЗаполните в .env (полная таблица — docs/backend/env.md):
| Переменная | Значение |
|---|---|
MAX_BOT_TOKEN |
токен бота (шаг 1) |
MAX_WEBHOOK_SECRET |
секрет вебхука (шаг 1) |
MAX_PUBLIC_WEBHOOK_URL |
https://<домен>/webhook/max |
B24_PORTAL |
домен портала, например your.bitrix24.ru |
B24_EVENT_HANDLER_URL |
https://<домен>/webhook/b24 |
B24_PLACEMENT_HANDLER_URL |
https://<домен>/b24/connector-settings |
B24_CLIENT_ID, B24_CLIENT_SECRET и OL_LINE_ID появятся на шагах 3–4. Поднимите стек и примените миграции:
COMPOSE_PROFILES=node,db-postgres docker compose up -d --build
docker compose --profile node exec api-node pnpm db:migrate
curl -s https://<домен>/health # {"status":"healthy",...} — сервер доступен снаружиmake dev-node делает то же, но в foreground с логами в терминале — тогда остальные команды запускайте во втором. Порты 8000 и Postgres публикуются только на 127.0.0.1; снаружи мост доступен через ваш reverse-proxy или туннель.
.envпопадает в контейнер только при его создании. После каждой правки.env:docker compose --profile node up -d --force-recreate api-node(restartего не перечитывает).
У моста нет интерфейса внутри Б24: токены портала приходят server-to-server на POST /api/install, когда вы сохраняете форму локального приложения.
-
На портале: Разработчикам → Другое → Локальное приложение. Отметьте «Приложение использует только API», путь для первоначальной установки —
https://<домен>/api/install, права (scope):Scope Зачем imopenlinesметоды imconnector.*иimopenlines.dialog.getimconnectorсобытия OnImConnectorMessageAdd/Update/DeleteиOnImConnectorDialogFinishimсистемные сообщения оператору ( im.message.add), нативный reply (im.dialog.messages.search) -
Сохраните форму. Б24 покажет
client_idиclient_secret— впишите их в.envкакB24_CLIENT_IDиB24_CLIENT_SECRETи пересоздайтеapi-node(команда выше). -
Сохраните форму ещё раз. Первое сохранение мост отклонил (
503, ключей ещё не было): подлинность установки он проверяет обменомrefresh_tokenс этими ключами. После второго в логахapi-node—install verified: tokens saved.
Подробности, коды ответов и переустановка — docs/backend/app-installation.md.
- В Контакт-центре создайте Открытую линию (или выберите существующую), куда пойдут сообщения групп.
- Узнайте её числовой ID методом
imopenlines.config.list.get— например, через входящий вебхук портала (Разработчикам → Другое → Входящий вебхук, правоimopenlines):curl -s 'https://<портал>/rest/<user_id>/<код вебхука>/imopenlines.config.list.get' | jq '.result[] | {ID, LINE_NAME}'
- Впишите ID в
.envкакOL_LINE_IDи пересоздайтеapi-node. Вебхук после этого можно удалить: мост работает на токенах приложения.
make register-connector # коннектор «Макс для групп» и его активация на линии OL_LINE_ID
make register-webhook # подписка MAX на MAX_PUBLIC_WEBHOOK_URL с секретом
make register-events # event.bind: сообщения, правки, удаления, закрытие диалога, удаление приложенияКаждый скрипт идемпотентен; ARGS="--list" показывает текущее состояние, ARGS="--unregister" — откат. Runbook'и: Connector Registration, MAX Webhook Setup.
⚠️ register-*перенаправляют вебхуки MAX и Б24 на URL из текущего.env. Если тот же бот или портал уже обслуживает другой стенд (например, прод), запуск с dev-окружения уведёт его вебхуки.
В приложении MAX откройте группу → Участники → Добавить участников → найдите бота. Затем назначьте его администратором с правами не меньше read_all_messages и write: без read_all_messages MAX не присылает боту события группы, без write бот не ответит (подготовка бота). Первого бота-администратора MAX позволяет назначить только вручную в мессенджере.
Если прав не хватает, бот один раз напишет об этом в группу (Admin Health-check). Мост работает только в группах: личка с ботом и каналы не подключаются.
- Напишите в группу — в Открытой линии появится чат с названием группы, сообщение — от имени автора.
- Ответьте оператором — ответ придёт в группу MAX с подписью оператора.
- В логах (
docker logs api) —delivered to Open Lineиdelivered to MAX.
| Симптом | Причина | Что делать |
|---|---|---|
curl https://<домен>/health не отвечает или ругается на сертификат |
домен не указывает на сервер, нет TLS или proxy не ведёт на 127.0.0.1:8000 |
проверить DNS, сертификат и reverse-proxy (пример Caddy — vps-prod.md §6) |
/api/install → 503 CONFIG_ERROR, токенов в БД нет |
форму сохранили до того, как ключи попали в .env, или ключи неверные |
вписать B24_CLIENT_ID/SECRET, пересоздать api-node, пересохранить форму — Б24 событие не повторяет |
/api/install → 401 UNAUTHORIZED |
чужой портал (B24_PORTAL) или повтор уже потраченного refresh_token |
проверить B24_PORTAL, пересохранить форму (коды ответов) |
/webhook/max → 401 SECRET_INVALID / SECRET_MISSING |
MAX_WEBHOOK_SECRET в .env не тот, с которым создана подписка |
make register-webhook ARGS="--unregister", поправить .env, пересоздать api-node, make register-webhook |
/webhook/max → 503 WEBHOOK_NOT_CONFIGURED |
MAX_WEBHOOK_SECRET пуст |
заполнить и пересоздать api-node |
/webhook/b24 → 401 B24_AUTH_INVALID |
событие не от нашего приложения или токен установки не сохранён | проверить установку (шаг 3) и make register-events ARGS="--list" |
/webhook/b24 → 503 B24_APP_UNINSTALLED |
приложение удалили на портале | переустановить (переустановка) |
| Сообщения из группы не доходят до ОЛ | у бота нет read_all_messages (не админ), это личка или канал, нет подписки MAX |
права админа (шаг 6); make register-webhook ARGS="--list"; логи api-node |
| Бота не добавить в группу | добавление в группы запрещено настройками бота | платформа для партнёров: бот → ⋮ → Настройки → «Приватность» (шаг 1) |
| Ответ оператора не уходит в MAX | нет event.bind на /webhook/b24 или нет scope imconnector |
make register-events ARGS="--list"; scope приложения (шаг 3) |
Правка .env не подействовала |
контейнер перезапущен, а не пересоздан | docker compose --profile node up -d --force-recreate api-node |
Скрипт на хосте: getaddrinfo ENOTFOUND database |
хост database резолвится только внутри compose-сети |
задать DATABASE_URL с localhost:<DB_PORT> в env процесса (env.md) |
Запросы к MAX: UNABLE_TO_GET_ISSUER_CERT_LOCALLY |
вне Docker нет корня Минцифры для platform-api2.max.ru |
NODE_EXTRA_CA_CERTS=backends/node/api/certs/russian_trusted_root_ca.pem или MAX_API_BASE_URL=https://botapi.max.ru (env.md) |
| Метод / путь | Назначение | Auth |
|---|---|---|
POST /webhook/max |
Входящие Update от MAX Bot API | X-Max-Bot-Api-Secret |
POST /webhook/b24 |
События Б24 (OnImConnectorMessageAdd и пр., ONAPPUNINSTALL — удаление приложения) |
application_token + member_id |
POST /api/install |
Server-to-server install-callback Б24 (ONAPPINSTALL); строгий rate-limit |
обмен refresh_token с B24_CLIENT_ID/SECRET |
GET /b24/connector-settings |
PLACEMENT_HANDLER для imconnector.register (заглушка настроек коннектора) |
— |
GET /health |
Health-check (публичный, без auth — для мониторинга и Caddy-проб) | — |
GET / |
API-only заглушка корня (Caddy catch-all → api-node); JSON-массив строк | — |
JWT-зоны нет: роутов стартера
POST /api/getTokenи JWT-onlyGET /api/healthбольше нет,JWT_*не читаются. Вебхуки и/api/installзащищены rate-limit.
Регистрационные/диагностические one-time скрипты (make <target> — внутри контейнера; pnpm <script> — из backends/node/api/ на хосте):
| Скрипт | Назначение |
|---|---|
register-connector |
Регистрация коннектора MAX в Б24 и активация на линии |
register-webhook |
Подписка MAX webhook'а |
register-events |
Привязка событий Б24: сообщения, правка, удаление, закрытие диалога, удаление приложения OnAppUninstall |
audit-bindings |
Read-only аудит привязок: тип каждого MAX-чата (GET /chats/{id}), лички и каналы среди привязок |
probe-imconnector-send, probe-max-send |
Проба отправки на живом портале и в живую группу |
probe-max-upload |
Проба 3-шагового upload-флоу MAX (медиа) |
probe-media-memory |
Замер RSS потока медиа (downloadFile → диск → uploadBinary) через loopback, без сети и .env — сравнение рантаймов Node |
Скрипт, запущенный на хосте (
pnpm <script>изbackends/node/api/), сам читает корневой.env— безexport.DATABASE_URLс хостомdatabaseс хоста не резолвится: задайте его в env процесса (Environment).
Основы backend
| Документ | Описание |
|---|---|
| Architecture | Слои/модули backend, как добавить модуль за 5 шагов |
| Migrations | SQL-runner, имена миграций, порядок на проде (только расширяющие, advisory lock), запуск на хосте, тестовая БД |
| Environment | Полная таблица env-переменных, коллизия PORT → API_PORT, корневой .env на хосте |
| Logging | Формат pino, requestId, redact-список |
| Webhooks | Паттерн проверки webhook'ов и идемпотентность |
Runbook'и
| Документ | Что внутри |
|---|---|
| App Installation | Install-callback POST /api/install: проверка подлинности, коды ответов, первая установка, негативные пробы; удаление (ONAPPUNINSTALL) и переустановка |
| Connector Registration | register-connector.js, prerequisites, права приложения, откат |
| MAX Webhook Setup | register-webhook.js, SECRET_MISSING/INVALID |
| Inbound Routing | MAX → Open Lines, авто-онбординг (атомарный), только группы, сообщение без mid, аудит привязок, skip-вердикты routeInbound |
| Outbound Routing | Open Lines → MAX, skip-вердикты routeOutbound, оповещение оператора о неудаче (notifyOperator) |
| Formatting + Delivery | BB↔markdown, delivery-статус |
| Admin Health-check | Проверка read_all_messages, dedup-guard |
| Media Transfer | Передача файлов в обе стороны + входящие голосовые, upload-флоу, retry, картинка файлом при отказе MAX |
| Membership Notifications | Системные уведомления о вступлении/выходе участников (im.message.add, миграция 0007) |
| Group Title & Icon | Название и иконка MAX-группы в чате ОЛ: интро от «группы», imconnector.chat.name.set, chat_title_changed, миграция 0010; ссылка на группу системным сообщением, CHAT_RENAMING_FAILED → флаг опенера |
| Group Session Opener | «Группа» — клиент каждой сессии ОЛ: OnImConnectorDialogFinish, опенер перед следующим входящим, миграция 0011; второй сигнал закрытия — CHAT_RENAMING_FAILED при переименовании группы |
| Reply / Quoting | Двусторонний мост «ответ»: >>-цитата MAX→OL, нативный reply OL→MAX (im.dialog.messages.search) |
| Message Editing | Двусторонний мост правки текста: imconnector.update.messages (MAX→OL) + PUT /messages (OL→MAX), маркер «Изменено», оповещение оператора о сбое |
| Message Deletion | Двусторонний мост удаления: imconnector.delete.messages (MAX→OL) + DELETE /messages (OL→MAX), оповещение оператора о неудаче (max-window, не полностью) |
| Outbound Queue | Очередь исходящих по чату (FIFO, лимит OUTBOUND_QUEUE_MAX, duplicate-in-progress), строки job: в message_log, остановка с дренажом и открытым портом, оповещение о передачах, прерванных рестартом |
| Deploy: VPS prod | Боевой VPS: Caddy + TLS, make deploy/make pull-prod-env (миграции до старта кода, догон FROM, --delete только по backends/, шаг up ждёт дренажа, health /health), рестарт и reboot, переезд на другой хост |
Участие: CONTRIBUTING.md — тесты, коммиты, правила архитектуры · SECURITY.md — как сообщить об уязвимости · контекст для AI-агентов — AGENTS.md.
MIT — см. LICENSE.