Привет! Меня зовут Игорь Росляков, я технический писатель. По приглашению руководителя направления «Маркет и интеграции» Сергея Вострикова готовлю цикл статей на тему ИИ-ассистированной разработки решений для Битрикс24

Почти все проекты мы делаем на основе одного репозитория b24-ai-starter, который служит базой для разработки приложений. В нём уже есть все инструкции для ИИ, которые облегчают работу агентов.

Статьи из цикла можно использовать как туториалы при создании локальных приложений для своего бизнес-портала. Их можно показать своим разработчикам или использовать самому, если работаете один и хотите кастомизировать работу дополнительными удобными функциями.

Сегодня — 5-я статья. Мы создадим чат-бота, зарегистрируем его в портале и научим делать что-нибудь полезное. Всю работу будем делать с помощью AI-агентов. Во время работы разберём все возникающие сложности и устройство приложения.

Ссылка на итоговый репозиторий, который уже подключен к порталу:
github.com/igorrosliakov-bitrix24/Bitrix24-ChatBot

Игорь Росляков

Технический писатель

Что было в предыдущих туториалах:

  1. Пишем первое приложение с AI-стартером, чтобы видеть прибыли и убытки

  2. Добавляем в бизнес-портал Битрикс24 роботов для автоматизации

  3. Что даёт воспроизводимая среда разработки и как развернуть контейнеры на VPS

  4. Анализ и модернизация коннектора баз данных с помощью AI-агентов

Что будет в этой статье:

Что нужно установить перед работой

Нам понадобятся:

  • Docker и Docker Compose для контейнеризации.

  • Git для контроля версий и пуша в удалённый репозиторий.

  • make для коротких команд.

  • Публичный HTTPS-туннель через CloudPub. Получить можно через десктопный клиент или инструмент командной строки clo.

Что за боты и где они находятся

В портале есть раздел Мессенджер. Там находятся все диалоги:

Если создать чат-бота, он тоже будет здесь. С ним можно начать диалог, попросить выполнить какие-то действия или запросить информацию.

Регистрируем приложение в портале

Сначала нужно прокинуть туннель через CloudPub, чтобы портал Битрикс24 мог обращаться к нашему приложению по HTTPS. Если не хотите через CloudPub, можно задеплоить на сервере — но это обычно дороже и дольше. Как копировать и настроить проект, прокинуть туннель с CloudPub и зарегистрировать приложение в своём портале, мы рассказали в первой статье в разделах:

При регистрации нужно указать путь обработчика и путь первоначальной установки. Для большинства проектов эти пути выглядит как URL от Cloudpub и тот же самый URL с эндпойнтом /install:

Но у чат-бота сценарий немного другой, поэтому оба пути должны быть одинаковыми и заканчиваться на эндпойнт /api/install. Ещё одно отличие: нужно выбрать пункт «Использует только API», потому что у чат-бота нет фронтенда:

Обновление: во время работы над статьёй в стартер внесли правку — теперь для приложений, которые используют только API, достаточно указывать эндпойнт /install. Если вы заметили, что можно исправить в стартер-ките, то тоже можете внести предложения по улучшению, потому что это open-source проект.

Если интересно, в чём разница пути обработчика и установки — объяснение под спойлером:

Путь обработчика, путь установки и почему у чат-бота именно такие параметры

Главное — для API-приложения на этих URL должен быть бэкенд-код, который понимает формат запроса. Одинаковые значения в двух полях допустимы, если один эндпойнт умеет обработать оба запроса.

Путь для первоначальной установки

Для обычного приложения с интерфейсом для установки нужно открывать фронтенд-страницу вида:

https://...cloudpub.ru/install

Но на этот раз наше приложение использует только API. В этой схеме при установке должен отправить данные установки или переустановки на наш бэкенд. Эти данные приходят HTTP-запросом: access token, refresh token, domain, member_id, scope. Бэкенд должен принять эти данные и понять: «Приложение установили в портал. Сейчас нужно сохранить токены, домен портала, member_id и выполнить стартовую настройку».

Путь вашего обработчика

Это более общий URL обработчика приложения. Битрикс24 может использовать его как основной серверный обработчик событий и запросов приложения.

В документации для чат-бота указан один PHP-файл:

https://example.com/chatapi/bot.php

И этот файл умеет обрабатывать разные случаи:

  • Установка приложения.

  • Сообщение боту.

  • Удаление бота.

То есть «путь обработчика» — это адрес, где находится серверная логика приложения.

Деплой на VPS мы разбирали это в отдельной статье: «Что даёт воспроизводимая среда разработки и как развернуть контейнеры на VPS».

Права приложения зависят от того, что должен уметь делать ваш чат-бот. Но минимальный набор прав для начала такой:

  • Веб-мессенджер (im) необходим для передачи сообщений пользователю.

  • Создание и управление Чат-ботами (imbot) для регистрации чат-бота.

Проверяем, что чат-бот появился в портале

После успешной регистрации чат-бота можно найти в мессенджере, если начать вводить его имя:

Но в моём случае от момента регистрации до появления чат-бота в портале прошло довольно много времени, потому что во время интеграции всплыло несколько небольших ошибок, которые агент последовательно устранял.

При интеграции редко бывает одна большая ошибка. Чаще это цепочка маленьких несовпадений: несколько маленьких мест, где ожидания документации, портала и нашего проекта расходятся.

Дальше я опишу основные проблемы интеграции, но сначала остановимся на том, как с ними работать.

Как работать с ошибками: смотреть логи и передавать их агенту

Лучшее, что вы можете сделать во время работы с агентом — сразу дать ему доступ к файлу логов Symfony/PHP бэкенда. Другой вариант — мониторить логи самостоятельно такой командой:

tail -f logs/php/symfony/dev.log

Мониторить логи самому полезно и интересно для понимания работы, но если важна скорость, достаточно дать агенту доступ. Он сам найдёт все ошибки и поправит их. 

Команда выше показывает логи PHP-приложения. Через неё мы видим действия нашего портала Битрикс24, которые дошли до нашего PHP-бэкенда. То есть если портал отправил запрос на наш endpoint, мы увидим его в Symfony-логе.

Что есть что в этой команде:

  • tail показывает конец файла.

  • -f означает «следить дальше в реальном времени», то есть команду можно запустить в одном терминале и просто копировать вывод терминала после каждой новой ошибки.

  • logs/php/symfony/dev.log — файл, куда PHP/Symfony пишет dev-логи.

А вот что можно увидеть в логах:

  • Входящие запросы от портала Битрикс24.

  • Какой Symfony route сработал.

  • payload, который прислал портал.

  • Наши debug-сообщения.

  • Ошибки PHP/Symfony.

  • Запросы нашего бэкенда к Битрикс24 REST API.

  • Ответы портала на эти API-запросы.

Например, запись Matched route "b24_install" означает, что Битрикс24 достучался до нашего /api/install. call.start {"apiMethod":"imbot.register"} показывает, что бэкенд вызывает REST API Битрикс24, а chatbotRegistered — что бот зарегистрирован.

Но по этой команде нельзя увидеть всё. Логи фронтенда, базы данных, Cloudpub и системные Docker-логи контейнеров вызываются отдельно. Но в моём конкретном случае нужны были именно логи PHP-приложения.

Ошибка 1: неверный формат install payload

Битрикс24 в режиме «Использует только API» присылал данные установки не в том формате, который ожидал стартер.

В процессе установки участвовал PHP-бэкенд. Но в коде репозитория AI-стартер был класс с названием FrontendPayload, и бэкенд использовал его для разбора данных установки. И вот этот класс ожидал такие поля:

DOMAIN
AUTH_ID
REFRESH_ID

А портал присылал вложенный объект:

auth.domain
auth.access_token
auth.refresh_token
auth.member_id

То есть проблема была в том, что класс ожидал payload из frontend-сценария установки, а портал в API-only режиме присылает другой формат. 

Обратите внимание, что стартер-кит периодически дорабатывается. В случае изменений можно отправить issue в репозиторий и сделать его лучше.

Как решилось: агент расширил FrontendPayload, чтобы он понимал оба варианта: frontend-style payload и direct API-only payload.

Ошибка 2: не хватало прав приложения

Права нужно отслеживать в 2 местах.

При регистрации приложения в портале:

Те же права должны быть прописаны в .env в параметре SCOPE:

SCOPE='im,imbot,task,tasks_extended,imconnector'

Если забыть выдать права, установка может упасть на REST-вызове с такой ошибкой:

insufficient_scope

Ошибка 3: записи в локальной базе данных

После нескольких попыток установки в PostgreSQL остались старые записи. При новой установке база сказала:

duplicate key value violates unique constraint

То есть в базе уже существовала запись для пары b24_user_id и domain_url.

Как решилось: агент просто почистил старые записи установки из локальной базы. Обратите внимание, что это нормально на этапе разработки, но в проде так делать нельзя. Для эксплуатации нужна аккуратная идемпотентная логика установки, когда один и тот же вызов не вызывает изменения дважды.

Ошибка 4: новый метод регистрации бота не работал на портале

Для регистрации предполагается использовать метод imbot.v2.Bot.register.

Но почему-то в моём портале это не сработало — видимо, агент неправильно прочитал документацию. В результате портал ответил:

ERROR_METHOD_NOT_FOUND
Method not found

Как решилось: агент решил испробовать метод, который использовался раньше, и он сработал.

imbot.register

В итоге агент добавил fallback: сначала для установки используется новый метод, а если он не срабатывает, пробуем старый.

Ошибка 5: разные состояния локальной базы данных и портала

В стартере есть обработчик lifecycle-событий AppLifecycleEventController. У него есть route:

#[Route('/api/app-events/', name: 'b24_events', methods: ['POST'])]:

То есть Symfony говорит: «Если придет POST-запрос на /api/app-events/, передай его в AppLifecycleEventController». 

Lifecycle-события — события жизненного цикла приложения: установили, обновили, удалили. И портал может сообщать бэкенду о таких событиях, например ONAPPINSTALL означает «приложение установлено в портале», а ONAPPUNINSTALL — наоборот, «приложение удалено из портала».

Во время установки наш бэкенд сообщил адрес обработчика lifecycle-событий Битрикс24, и после этого портал начал отправлять события вроде ONAPPINSTALL на отдельный endpoint /api/app-events/.

В чём была проблема: после нескольких неудачных попыток установки агент почистил все записи из локальной базы данных — это было на этапе Ошибки №3. Получилось так, что локальная база и состояние портала разошлись: в портале обработчик уже привязан, а в базе записи установки нет. 

В итоге Битрикс24 прислал событие установки, но наш бэкенд не нашел соответствующую запись установки в локальной базе. Появилась новая ошибка:

Application installation not found

Как решилось: обработчик lifecycle-событий должен быть устойчивым: отсутствие локальной записи не должно ронять весь процесс. 

Раньше пайплайн установки выглядел примерно так:

Если пришел ONAPPINSTALL:
  обязательно найди установку в базе
  если не нашел -- ошибка 500

А стал выглядеть так:

Если пришел ONAPPINSTALL:
  попробуй финализировать установку в базе
  если записи нет -- запиши warning, но не падай
  продолжай обработку

Ещё агент сделал так, что событие ONAPPINSTALL использовалось как второй шанс для регистрации чат-бота, потому что оно содержит полезные данные:

  • auth.access_token

  • auth.domain

  • auth.member_id

  • data.VERSION

То есть пайплайн ещё немного изменился:

Если регистрация бота не завершилась в /api/install:
  попробуй зарегистрировать его при обработке ONAPPINSTALL в /api/app-events/.

В итоге помогло именно это. В логе появились такие записи:

AppLifecycleEventController.process.chatbotRegistered
method: imbot.register
result: ["14"]

А сам чат-бот появился в мессенджере.

PS Откуда в проекте фронтенд

В логах и при работе с агентами можно заметить, что периодически встречаются записи и ошибки со словом frontend. Это может сбивать с толку, поэтому на всякий случай дадим краткое объяснение.

В AI-стартере есть отдельная frontend-часть на Nuxt, и для нее есть отдельный Docker-контейнер. Это потому, что стартер рассчитан не только на чат-ботов. Он может использоваться для обычных Битрикс24-приложений, где пользователь открывает интерфейс внутри портала — таких, как наши приложения с дашбордами и роботами.

То есть фронтенд нужен, если приложение имеет экран, кнопки, формы, настройки, виджеты, placement и так далее. Но если приложение использует только API, как чат-бот, фронтенд-контейнер может быть запущен, но всё будет работать и без него.

Учим чат-бота полезным функциям

Сейчас боту можно написать, но он ничего не ответит. Чтобы он мог делать что-то нужное, для него нужно создать возможные команды.

Агента можно попросить добавить функции в свободной форме, например: «Научи бота собирать просроченные задачи и присылать мне отчёт по команде, который будет включать дни просрочки и имена ответственных». Понимать код для добавления этих возможностей необязательно, но мы всё равно разберём, как это работает.

Главный файл для работы — ChatbotEventsController.php.

Общая логика работы с командами
#[Route('/api/chatbot/events', name: 'chatbot_events', methods: ['POST'])]
public function process(Request $request): JsonResponse
{
    // 1. Получаем событие от Битрикс24: сообщение, вход в чат и т.д.
    $payload = $this->getPayload($request);

    // 2. Нас интересуют только события чат-бота.
    if (!in_array($payload['event'] ?? '', ['ONIMBOTJOINCHAT', 'ONIMBOTMESSAGEADD'], true)) {
        return new JsonResponse(['status' => 'ignored'], 200);
    }

    // 3. Достаем текст сообщения пользователя.
    // 4. Определяем команду.
    // 5. Строим ответ.
    $reply = 'ONIMBOTJOINCHAT' === ($payload['event'] ?? '')
        ? $this->buildHelpReply()
        : $this->buildReply($this->extractUserMessage($payload), $payload);

    // 6. Отправляем ответ обратно в Битрикс24.
    $this->sendReply($payload, $reply);

    return new JsonResponse(['status' => 'ok'], 200);p

Команды добавляются в методе buildReply.

Общая логика работы бота в зависимости от разных команд
$tokens = $this->extractTokens($message);
$command = mb_strtolower($tokens[0] ?? '');
$arguments = array_slice($tokens, 1);

// Если команда /fx -- идем в валютный функционал.
if (in_array($command, ['/fx', 'fx', 'валюта', 'курс'], true)) {
    return $this->buildFxReply($arguments);
}

// Если команда /stock -- идем в функционал акций.
if (in_array($command, ['/stock', 'stock', 'акция', 'тикер'], true)) {
    return $this->buildStockReply($arguments);
}

// Если команда /market -- собираем акции + валюты.
if (in_array($command, ['/market', 'market', 'рынок', 'инвестиции'], true)) {
    return $this->buildMarketReply($arguments);
}

// Если команда /утро -- идем в Битрикс24 за задачами.
if (in_array($command, ['/morning', 'morning', '/утро', 'утро', 'дайджест'], true)) {
    return $this->buildMorningReply($payload);
}

То есть общие правила добавления команд такие:

1. Добавить условие в buildReply().

2. Создать метод buildSomethingReply().

3. Если нужна сложная логика — вынести ее в отдельный Service.

4. Вернуть строку, которую бот отправит в чат.

Запрос статуса задач

Сначала мы добавили отчёт по всем существующим задачам. Пользователь может написать слова: /morning, morning, /утро, утро, дайджест, и бот запускает функцию buildMorningReply:

if (in_array($command, ['/morning', 'morning', '/утро', 'утро', 'дайджест'], true)) {
    return $this->buildMorningReply($payload);
}

Что делает buildMorningReply:

private function buildMorningReply(array $payload): string
{
    // Достаем из события Битрикс24 домен, access token и ID пользователя.
    $context = $this->extractBitrixContext($payload);

    // Передаем эти данные в сервис, который умеет читать задачи.
    return $this->taskDigestService->buildMorningDigest(
        $context['domain'],
        $context['accessToken'],
        $context['userId'],
    );
}

Контекст по задачам получает функция extractBitrixContext:

   private function extractBitrixContext(array $payload): array
   {
       $bots = (array) ($payload['data']['BOT'] ?? []);
       $botAuth = [] !== $bots ? reset($bots) : [];
       $botAuth = is_array($botAuth) ? $botAuth : [];

       $domain = (string) ($payload['auth']['domain'] ?? $botAuth['domain'] ?? '');
       $accessToken = (string) ($payload['auth']['access_token'] ?? $botAuth['access_token'] ?? '');
       $userId = (int) ($payload['data']['USER']['ID'] ?? $payload['auth']['user_id'] ?? 0);

       if ('' === $domain  '' === $accessToken  0 === $userId) {
           throw new \RuntimeException('Не хватает данных Битрикс24 для чтения задач.');
       }

       return [
           'domain' => $domain,
           'accessToken' => $accessToken,
           'userId' => $userId,
       ];
   }

Общая логика задач находится уже в другом файле — Bitrix24TaskDigestService.php:

Общая логика задач
public function buildMorningDigest(string $domain, string $accessToken, int $userId): string
{
    // Загружаем активные задачи пользователя из Битрикс24.
    $tasks = $this->loadActiveTasks($domain, $accessToken, $userId);

    // Готовим группы задач.
    $overdue = [];
    $todayTasks = [];
    $important = [];
    $withoutDeadline = [];
    $future = [];

    // Раскладываем задачи по категориям.
    foreach ($tasks as $task) {
        if ($this->isCompleted($task)) {
            continue;
        }

        if (($task['priority'] ?? '') === '2') {
            $important[] = $task;
        }

        $deadline = $this->parseDeadline($task['deadline'] ?? null);

        if (null === $deadline) {
            $withoutDeadline[] = $task;
            continue;
        }

        // Сравниваем дедлайн с сегодняшним днем.
    }

    // Собираем текст ответа.
    return implode("\n", $lines);
}

Последняя часть — запрос к порталу в функции loadActiveTasks:

$this->httpClient->request('POST', sprintf('https://%s/rest/tasks.task.list', $domain), [
    'json' => [
        // Берем задачи, где текущий пользователь ответственный.
        'filter' => [
            'RESPONSIBLE_ID' => $userId,
            '!REAL_STATUS' => 5,
        ],

        // Запрашиваем только нужные поля.
        'select' => [
            'ID',
            'TITLE',
            'STATUS',
            'REAL_STATUS',
            'DEADLINE',
            'PRIORITY',
        ],

        // Токен Битрикс24 из входящего события бота.
        'auth' => $accessToken,
    ],
]);

Что получается при запуске:

Запрос во внешние API

Бот может работать не только с инструментами портала, но и внешними источниками. Для примера я попросил чат-бота ходить за курсами акций и валют в 2 разных API.

Команды для бота находятся там же, где и команда для сводки задач из портала — в файле ChatbotEventsController.php:

// Валюты: /fx USD EUR
if (in_array($command, ['/fx', 'fx', 'валюта', 'курс'], true)) {
    return $this->buildFxReply($arguments);
}

// Акции: /stock AAPL MSFT
if (in_array($command, ['/stock', 'stock', 'акция', 'тикер'], true)) {
    return $this->buildStockReply($arguments);
}

// Общая сводка: /market AAPL MSFT USD EUR
if (in_array($command, ['/market', 'market', 'рынок', 'инвестиции'], true)) {
    return $this->buildMarketReply($arguments);
}Ъ

Формирование ответа по акциям происходит в функции buildStockReply:

foreach (array_slice($arguments, 0, 5) as $symbol) {
    // Пропускаем валюты, оставляем только тикеры акций.
    if ($this->marketDataService->isCurrency($symbol)) {
        continue;
    }

    // Получаем котировку акции.
    $quotes[] = $this->marketDataService->getStockQuote($symbol);
}

Общая сводка создаётся в buildMarketReply:

foreach ($arguments as $argument) {
    // Если это валюта -- кладем в currencySymbols.
    if ($this->marketDataService->isCurrency($argument)) {
        $currencySymbols[] = $argument;

    // Если похоже на тикер -- кладем в stockSymbols.
    } elseif (preg_match('/^[A-Z]{1,6}(?:\.[A-Z]{1,4})?$/', $argument)) {
        $stockSymbols[] = $argument;
    }
}

Внешние API вынесены в отдельный файл MarketDataService.php. Бот получает валюты через сервис Frankfurter в getExchangeRate:

$payload = $this->httpClient
    ->request('GET', 'https://api.frankfurter.app/latest', [
        'query' => [
            'from' => $base,
            'to' => $target,
        ],
    ])
    ->toArray();

Курсы акций бот пытается получить через 2 разных API: Alpha Vantage или Stooq. Alpha Vantage даёт более надёжную информацию, но для него нужен ключ. Если мы захотим использовать его в будущем, этот ключ нужно будет положить в .env

ALPHA_VANTAGE_API_KEY='твой_ключ'

Если ключа нет, используем более простой сервис Stooq:

$apiKey = (string) ($_ENV['ALPHA_VANTAGE_API_KEY'] ?? $_SERVER['ALPHA_VANTAGE_API_KEY'] ?? '');

if ('' !== $apiKey) {
    return $this->getAlphaVantageQuote($symbol, $apiKey);
}

return $this->getStooqQuote($symbol);

Чтобы протестировать работу, нужно вызывать команду и аргументы. Командой может быть одно из слов в списке:

'/market', 'market', 'рынок', 'инвестиции'

И сразу после этого передаём аргументы — названия акций и валют в том виде, как они приняты на рынке. Например, /market AAPL MSFT USD EUR:

Или так — /market TSLA USD JPY:

Что будем делать дальше

Стартер-кит упрощает и ускоряет работу с порталом Битрикс24. В следующий раз мы подробнее разберём, почему это стандарт разработки и создадим ещё что-нибудь полезное и интересное.

Если вам интересна какая-то тема про кастомизацию портала — напишите в комментариях, а мы постараемся раскрыть её в следующих статьях.

Содержание цикла статей про создание приложений с AI-агентами

  1. Пишем первое приложение с AI-стартером, чтобы видеть прибыли и убытки

  2. Добавляем в бизнес-портал Битрикс24 роботов для автоматизации

  3. Что даёт воспроизводимая среда разработки и как развернуть контейнеры на VPS

  4. Анализ и модернизация коннектора баз данных с помощью AI-агентов

  5. Создание чат-бота в портал Битрикс24 с помощью AI-агентов

Комментарии (0)