AI 26.1200.0: запись истории ИИ-запросов через REST включает только администратор
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
В модуле 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()подписан на событиеfilemanHtmlEditor: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()). В коробке без модуляbitrix24Facade\Bitrix24::isFeatureEnabled()отвечает true, поэтому план там есть всегда.Usage::getAvailablePeriods()иgetPeriods()сталиprotected.QueueJob::fail()иexpire()вызываютrollbackConsumption()без проверкиcheckLimits()(lib/QueueJob.php). При пустомconsumptionIdоткат уходит вUsage::decrement(), так что сбой записи вb_ai_usageможет всплыть и при провале задачи очереди.- В режиме
MonthlyPoolUsage::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'. PHPDocVibePlusUpsellService::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.