ai 26.1200.0 Безопасность

AI 26.1200.0: запись истории ИИ-запросов через REST включает только администратор

3 мин чтения
Узнавайте о ломающих изменениях Битрикса раньше, чем они доедут до прода: свежие разборы — в канале @bxmax_updates

Обновление безопасности

Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.

В модуле ai коробочного Битрикс24 версии 26.1200.0 REST-методы ai.history.enable и ai.history.disable пускают только администратора. Они включают и выключают запись истории всех ИИ-запросов портала, а проверки администратора в них до этой версии не было. Ещё модуль перестал подключаться на лицензиях региона cn, а Limiter\Usage бросает исключение, если не смог записать счётчик расхода.

Остальной объём релиза коробку почти не задевает. Из 6457 строк, добавленных в bitrix/, 3109 даёт один собранный copilot.bundle.js, а новая тарифная политика лимитов и сообщения тарифа Vibe+ работают только с модулем bitrix24, которого в коробке нет. Читать стоит тем, кто вызывает ai.history.* из интеграций или шлёт ИИ-запросы из своего кода через Engine.

Что может сломаться

Сигнатуры совместимы. В API добавились только новые методы и необязательные параметры конструкторов ExceededLimitDto, Usage и LimitControlService, новых @deprecated нет. Меняется поведение.

Проверьте, от чьего имени вызывается ai.history.enable

ai.history.enable ставит опции модуля write_history_always, write_history_request и write_errors в Y, ai.history.disable возвращает их в N. С этими опциями портал пишет историю всех ИИ-запросов. Оба метода зарегистрированы в скоупе ai_admin (lib/Rest.php).

Теперь History\Rest::enable() и disable() (lib/History/Rest.php) первым делом вызывают новый приватный checkAdminAccess(). Если Facade\User::isAdmin() вернул false, метод бросает \Bitrix\Rest\AccessException, и опции остаются как были. Вебхук или приложение, которое включало историю от имени обычного сотрудника, после обновления получит ошибку доступа. Такой вебхук лучше перевыпустить от имени администратора.

Проверку получили только эти два метода. ai.history.list, ai.prompt.register и ai.engine.* из того же скоупа работают без неё, как раньше.

Проверяйте includeModule(’ai’) перед вызовом Bitrix\AI

include.php модуля теперь начинается с проверки региона лицензии. Он читает Application::getInstance()->getLicense()->getRegion(), при null подставляет 'en' и для cn делает return false. На такой лицензии Loader::includeModule('ai') вернёт false, загрузчик снимет регистрацию неймспейса модуля и не зарегистрирует сервисы из его .settings.php (bitrix/modules/main/lib/loader.php, строки 154–163 на коммите релиза). Код, который обращается к Bitrix\AI\... без проверки, классов не найдёт.

Ловите RuntimeException при отправке запроса

Limiter\Usage ведёт счётчики расхода ИИ-запросов. Раньше он отбрасывал результат UsageTable::add() и update(), и неудачная запись проходила молча. Теперь ошибка уходит в лог, после чего Usage бросает \RuntimeException('Unable to write AI usage.'). Engine вокруг commitRequest() ловит только SharedMonthlyPoolLimitExceededException, так что это исключение долетит до вашего кода. Касается это только движков, у которых checkLimits() возвращает true: наследников Engine\Engine без переопределения (в коробке это Triton\Classify) и REST-движка ThirdParty с кодом itsolutionru.gptconnector. У облачных движков CloudEngine::checkLimits() возвращает false, и штатные текстовые движки коробки b_ai_usage в completions() не трогают. Если периодов больше одного, Usage пишет их в транзакции соединения UsageTable и при ошибке откатывает её.

Проверку модуля и перехват исключения можно собрать в одном сервисе. Со штатным облачным движком категории text catch в коробке не сработает и ничего не стоит. Он нужен, если категорию обслуживает движок с checkLimits() === true:

        <?php declare(strict_types=1);

namespace Vendor\Assistant\Application\Service;

use Bitrix\AI\Context;
use Bitrix\AI\Engine;
use Bitrix\AI\Payload\Text;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;
use Psr\Log\LoggerInterface;

final class AiTextRequestService
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function send(string $prompt, int $userId, callable $onSuccess, callable $onError): Result
    {
        $result = new Result();

        // на лицензии региона cn модуль не подключится, и классов Bitrix\AI не будет
        if (!Loader::includeModule('ai')) {
            return $result->addError(new Error('Модуль ai недоступен', 'AI_MODULE_UNAVAILABLE'));
        }

        $engine = Engine::getByCategory('text', new Context('vendor.assistant', 'summary', $userId));
        if ($engine === null) {
            return $result->addError(new Error('Нет доступного текстового движка', 'AI_ENGINE_NOT_FOUND'));
        }

        try {
            $engine
                ->setPayload(new Text($prompt))
                ->onSuccess($onSuccess)
                ->onError($onError)
                ->completions();
        } catch (\RuntimeException $e) {
            // с 26.1200.0 так приходит сбой записи в b_ai_usage у движков с checkLimits() === true
            $this->logger->error('AI request aborted: {message}', ['message' => $e->getMessage()]);

            return $result->addError(new Error('Не удалось записать расход ИИ-запросов', 'AI_USAGE_NOT_WRITTEN'));
        }

        return $result;
    }
}

    

Подробности сбоя Usage пишет в логгер ai.limiter.usage. Чтобы они оседали в файле, заведите его в секции loggers файла bitrix/.settings.php:

        'loggers' => [
    'value' => [
        'ai.limiter.usage' => [
            'constructor' => static fn () => new \Bitrix\Main\Diag\FileLogger(
                '/var/log/bitrix/ai-usage.log',
            ),
            'level' => \Psr\Log\LogLevel::WARNING,
        ],
    ],
    'readonly' => true,
],

    

Одной секции мало. Логгер создаётся, только если его id включён в реестре, поэтому включите его один раз:

        (new \Bitrix\Main\Diag\LoggerRegistry())->enable('ai.limiter.usage');

    

Вызов ставит опцию loggers / logger_is_enabled_ai.limiter.usage в Y.

Уборка старых строк тоже поменялась. Раньше Usage удалял у пользователя любую строку, код периода которой не совпал с текущими. Теперь код разбирается как Y-m-d или Y-m, и строка удаляется, только когда её день или месяц уже закончился. Строки с кодом другого формата не удаляются никогда.

Новая политика лимитов и Vibe+ в коробке выключены

Limiter\Policy\TariffLimitPolicy выбирает режим лимитов из enum LimitPolicyMode: Legacy, Transition, Unlimited, MonthlyPool или DailyOnly. Первые три проверки в resolveMode() возвращают Legacy, если опция ai / tariff_limit_policy_enabled не равна Y (по умолчанию N), модуль bitrix24 не подключается или RuntimeStateProvider::isVibePlusTariffLineAvailable() ложно. Любое исключение в resolveMode() тоже даёт Legacy. В коробке модуля bitrix24 нет, так что режим там всегда Legacy при любом значении опции, а Facade\Bitrix24::isNewLimitPolicyActive() возвращает false.

С модулем bitrix24 дальше решают опция check_limits, фичи тарифа ai_unlimited_by_version, ai_limits_transition_period, ai_monthly_pool_by_version и переменная тарифа ai_query_limit_day. Режимы работают так (lib/Limiter/LimitControlService.php, lib/Limiter/Usage.php, lib/Limiter/Period/Daily.php):

  • Unlimited: периодов нет, isInLimit() всегда true, счётчики не пишутся, пакеты BaaS не тратятся;
  • DailyOnly: считается только Daily, потолок из getAiDailyLimit(), прежний расчёт переехал в Daily::getLegacyMaximumUsage();
  • MonthlyPool: один месячный пул на весь портал;
  • Transition: прежняя логика целиком, вместе с BaaS.

Пул ведёт Limiter\SharedMonthlyPoolService, он зарегистрирован в секции services модуля. Расход пула считается как сумма USAGE_COUNT всех пользователей по коду текущего месяца в b_ai_usage. При резерве запроса canConsumeSharedMonthly() проверяет остаток без блокировки. При списании tryConsumeSharedMonthly() берёт именованную блокировку Connection::lock('shared_monthly_pool', 5), пересчитывает сумму и увеличивает строку пользователя. releaseSharedMonthly() возвращает единицы при откате. На время работы все три включают ConnectionPool::useMasterOnly(true), а исключения глотают и пишут в логгер ai.limiter.shared_monthly_pool. Если в commitRequest() списать не удалось (пул исчерпан, блокировку не дали за 5 секунд или запись не прошла), летит SharedMonthlyPoolLimitExceededException, и Engine превращает его в обычную ошибку лимита. Схему можно повторить для квоты в своём модуле.

Ключ vibePlusLimitState в ошибке лимита

В коробке этот ключ не приходит. Фасад Facade\Portal берёт данные о Vibe+ из \Bitrix\Bitrix24\Public\Service\VibePlus\RuntimeStateProvider, а без модуля bitrix24 до этого класса не доходит и на проверки состояния Vibe+ отвечает false. Тогда resolveState() возвращает NotApplicable, и Engine::throwError() ключ не добавляет. Предупреждение в лог ai.vibe_plus.runtime_state (один раз за хит) пишется, только когда bitrix24 есть, а класса в нём ещё нет.

На облачных порталах западной зоны в customData ошибки LIMIT_IS_EXCEEDED может прийти vibePlusLimitState с именем кейса enum Enum\VibePlusLimitState: TechnicalLimit, BuyWithDemo или BuyWithoutDemo. Ставят его Engine::throwError() и ExceededLimitService::getErrorsLimitRules(). При TechnicalLimit Engine::throwError() не кладёт в ошибку sliderCode и ставит showSliderWithMsg в false.

Штатный JS под это переделан. AjaxErrorHandler при TechnicalLimit не открывает слайдер ни на месячном, ни на дневном лимите. Текстовый и графический контроллеры копилота в этом случае не закрываются и показывают текст ошибки в поле ввода. На дневном лимите слайдер теперь берётся из sliderCode, если там limit_why_pay_tariff_vibe, а раньше всегда открывался limit_copilot_max_number_daily_requests. У BX.AI.AjaxErrorHandler появились статические isVibePlusTechnicalLimit(customData) и getVibePlusTechnicalLimitMessage(customData). Второй вырезает из msgForIm BBCode-ссылку [url=...]...[/url], потому что копилот показывает ошибку простым текстом.

БД: проверьте индекс на b_ai_usage

В install/migrations/tables.php у b_ai_usage появился индекс IX_B_AI_USAGE_PERIOD по USAGE_PERIOD. На него опирается запрос SUM(USAGE_COUNT) ... WHERE USAGE_PERIOD = ? из SharedMonthlyPoolService::getCurrentUsage(). Файл описывает чистую установку. Как индекс попадёт на уже установленные порталы, по коду модуля не видно, потому что скрипт обновления в папке модуля не остаётся. На MySQL проверить можно так:

        SHOW INDEX FROM b_ai_usage WHERE Key_name = 'IX_B_AI_USAGE_PERIOD';

    

Мелочи и находки

  • Триггер бизнес-процессов aiagentstarttrigger (install/activities/bitrix/aiagentstarttrigger/) отдаёт новое поле EventDateTime типа DATETIME. В нём момент выполнения триггера (new DateTime()), время события из контекста туда не попадает.
  • Integration\Fileman\HtmlEditorHandler::onBeforeBuild() подписан на событие fileman HtmlEditor:onBeforeBuild прямо в include.php через EventManager::addEventHandler(), без регистрации в БД. Он дописывает в редактор расширение ui.design-tokens.air и градиентную заливку для узлов [bxhtmled-copilot-result-node="true"]. Этот обработчик и редизайн копилота BitrixGPT под классом --bitrixgpt-redesign требуют модуль aiassistant, а его в коробке тоже нет.
  • Facade\Bitrix24::getAgentDailyLimit() читает переменную тарифа ai_agent_query_limit_day, но нигде не вызывается. Отдельного дневного лимита для ИИ-агентов пока нет, похоже на заготовку.
  • Monthly::getMaximumUsage(): int раньше возвращал Plan::createByB24()?->getMaxUsage() и без плана упал бы с TypeError. Теперь он отдаёт 0, а сам Monthly попадает в список периодов, только если план есть (новый Usage::hasPlan()). В коробке без модуля bitrix24 Facade\Bitrix24::isFeatureEnabled() отвечает true, поэтому план там есть всегда. Usage::getAvailablePeriods() и getPeriods() стали protected.
  • QueueJob::fail() и expire() вызывают rollbackConsumption() без проверки checkLimits() (lib/QueueJob.php). При пустом consumptionId откат уходит в Usage::decrement(), так что сбой записи в b_ai_usage может всплыть и при провале задачи очереди.
  • В режиме MonthlyPool Usage::deleteForUser() оставляет строку текущего месяца, так что удаление пользователя не возвращает его расход в общий пул.
  • Portal::isVibePlusLicenseType() относит к линейке Vibe+ тарифы с суффиксом _vibe и demo. PHPDoc предупреждает, что безлимит ИИ по ней не определить: у basic_vibe его нет, у nfr он есть вне линейки. Квоты поэтому надо решать по фиче ai_unlimited_by_version.
  • В ExceededLimitService прямо написано, что ветка Vibe+ «not reached in shipped configurations at all». Маппер регистрируется только без модуля bitrix24, а гейт Vibe+ этот модуль требует. Ветку оставили для ai.use_bitrix24 = 'N'. PHPDoc VibePlusUpsellService::resolveLimitMessage() обещает фразы в lang-файлах обоих путей лимита, но ключи AI_ENGINE_ERROR_LIMIT_VIBE_* лежат только в lang-файле самого сервиса.
  • Комментарий в Engine::throwError() объясняет, зачем западная зона проверяется отдельно от гейта Vibe+. Модель монетизации берётся из имени группы контроллера, а isWestZone() смотрит на регион лицензии. При пустом имени группы модель говорит «Vibe+», а регион откатывается в ru.
  • В Usage::updateCount() сразу после if ($availablePeriods === []) return; стоит проверка if ($availablePeriods !== []), которая всегда истинна.
  • Новые id логгеров: ai.limiter.tariff_limit_policy, ai.limiter.shared_monthly_pool, ai.limiter.usage, ai.vibe_plus.runtime_state.
  • У восьми PHP-файлов, среди них .settings.php, History/Rest.php и Usage.php, права сменились с 100755 на 100644.

Что делать

  • Найдите в интеграциях вызовы ai.history.enable и ai.history.disable и проверьте, что вебхук или приложение работает от администратора.
  • Перед обращением к Bitrix\AI\... проверяйте результат Loader::includeModule('ai').
  • Если запросы идут через движок с checkLimits() === true, оберните их отправку через Engine в try/catch (\RuntimeException), заведите логгер ai.limiter.usage и включите его через LoggerRegistry.
  • После обновления проверьте индекс IX_B_AI_USAGE_PERIOD на b_ai_usage.

Новая версия вышла — разбор уже в Telegram

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

Подписаться

Читайте дальше

ai 26.1500.0 Безопасность Свежее

AI 26.1300.0–26.1500.0: закрыта XSS в имени роли копилота, ответ из кеша больше не даёт ложный сбой очереди

В коробочный Битрикс24 пришли три версии модуля `ai`: 26.1300.0, 26.1400.0 и 26.1500.0. Обновиться стоит ради 26.1500.0. В нём меню копилота перестало вставлять имя пользовательской роли в страницу как HTML, а такие роли расшаривают на отделы и группы. В 26.1400.0 очередь ИИ-запросов перестала слать…

2 мин
ai 26.1100.0 Заметное Свежее

AI 26.1100.0: срок жизни задачи очереди ИИ-запросов задаётся на запрос

В модуле `ai` коробочного Битрикс24 версии 26.1100.0 срок жизни задачи очереди ИИ-запросов больше не зашит в четыре часа. Его задают на запрос через `Engine::setQueueJobTtl()`, момент истечения пишется в новую колонку `b_ai_queue.EXPIRE_DATE`, а просроченную задачу `QueueJob::createFromHash()` тепер…

4 мин
ui 26.693.0 Заметное Свежее

UI 26.693.0: загрузчик называет причину отказа, а вьювер прячет кнопку скачивания

В ui 26.693.0 загрузчик файлов перед сохранением ещё раз проверяет временный файл и при отказе отдаёт текст ошибки из `CFile::CheckFile`, а при превышении дисковой квоты кладёт в ошибку причину `disk_quota_exceeded`. Вьювер теперь прячет кнопку скачивания у отдельного файла, а `FeaturePromoter.show(…

3 мин
socialnetwork 26.250.0 Рутинное Свежее

Socialnetwork 26.250.0: комментарий о том, как дочерние чаты проекта попадают в папку «Все»

В socialnetwork 26.250.0 код не менялся, проверять после обновления нечего. В двух внутренних обработчиках, `SendChildAttachedMessage` и `SendChildDetachedMessage`, появился одинаковый docblock о том, как дочерний чат проекта попадает в папку «Все» мессенджера. Push `chatUpdate` только убирает строк…

1 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…

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

Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти