note 26.400.0 Ломающее

note 26.400.0: updatedAt не отражает правки в редакторе, уведомления выключены по умолчанию

2 мин чтения

Ломающее обновление

Удалены или изменены публичные API — прикладной код может перестать работать.

В note 26.300.0 модуль «Базы знаний 2.0» получил события, историю версий и подписки. Версия 26.400.0 добавляет избранное, обратные ссылки между документами, отдельную материализацию markdown-проекции Yjs-документа, асинхронный пересчёт поиска и боковой чат BitrixGPT. Большую часть диффа снова занимает JS: editor.bundle.js, sidebar.bundle.js и новое расширение bitrix/js/note/ai-chat.

Для интеграций в нём ломаются пять вещей. updatedAt в REST v3 не отражает правки из редактора, уведомления по умолчанию не рассылаются, подписаться на документ без избранного нельзя, поиск догоняет правки агентом, а в конструкторах нескольких команд сдвинулись параметры. Пути ниже указаны от bitrix/modules/note/.

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

Синхронизируйте документы по contentUpdatedAt

В 26.300 компакция сохраняла документ через DocumentRepository::save(), тот выставлял UPDATED_AT, и отдельно вызывался setUpdatedBy(). Теперь проекция пишется через materializeProjection() и updatePartial(), а они UPDATED_AT не двигают (lib/Public/Command/CompactDocumentCommand.php). Поэтому updatedAt и updatedBy документа в REST v3 больше не отражают правки в редакторе (перезапись через REST note.document.update их по-прежнему двигает). Интеграция, которая синхронизирует документы по updatedAt, пропустит изменения из редактора.

Время правки текста отдаёт новое read-only поле contentUpdatedAt в DocumentItemDto для REST v3 и в Public\Provider\Dto\DocumentReadDto. У старых документов оно равно null, пока проекция не пересоберётся, так что берите более позднее из двух полей:

        <?php declare(strict_types=1);

namespace Vendor\KbSync\Rest;

final class NoteDocumentStamp
{
    /** Момент последнего изменения документа по ответу REST v3 note.document.* */
    public static function changedAt(array $item): ?\DateTimeImmutable
    {
        $stamps = array_filter([$item['updatedAt'] ?? null, $item['contentUpdatedAt'] ?? null]);
        if ($stamps === []) {
            return null;
        }

        return max(array_map(static fn (string $s): \DateTimeImmutable => new \DateTimeImmutable($s), $stamps));
    }
}

    

Включите доставку уведомлений

Configuration::isNotificationsUiEnabled() удалён, вместо него isNotificationsEnabled(). Опция осталась прежней, notifications_enabled со значением N по умолчанию. В 26.300 она прятала только колокольчик, а теперь выключает и доставку. NotificationDrainAgent::run() пропускает тик, пока опция не равна Y.

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

        <?php declare(strict_types=1);

use Bitrix\Main\Config\Option;

Option::set('note', 'notifications_enabled', 'Y');

    

Класс Internal\Configuration модуль к публичному API не относит. Если вы всё-таки проверяли флаг из своего кода, замените вызов на isNotificationsEnabled().

Добавьте документ в избранное до подписки

Для положительных режимов подписки SetSubscriptionCommand теперь требует запись в избранном. Без неё команда ничего не пишет и возвращает errorCode = FAVORITE_REQUIRED в данных Result, исключения нет. SubscriptionController::setAction в том же случае отдаёт ошибку FAVORITE_REQUIRED. Заглушить документ (mute) можно и без избранного. Добавлять в избранное нужно командой AddFavoriteCommand.

Отказ лежит в getData(), поэтому проверка isSuccess() его не заметит. А исключение из execute() базовый AbstractCommand::run() завернёт в CommandException. Обёртка, которая сводит оба случая к обычным ошибкам Result:

        <?php declare(strict_types=1);

namespace Vendor\KbSync\Note;

use Bitrix\Main\Command\AbstractCommand;
use Bitrix\Main\Command\Exception\CommandException;
use Bitrix\Main\Error;
use Bitrix\Main\Result;

final class NoteCommandRunner
{
    public function run(AbstractCommand $command): Result
    {
        try {
            $result = $command->run();
        } catch (CommandException $e) {
            $cause = $e->getPrevious() ?? $e;

            return (new Result())->addError(new Error($cause->getMessage(), $cause::class));
        }

        $code = $result->getData()['errorCode'] ?? null;
        if (is_string($code)) {
            return (new Result())->addError(new Error($code, $code));
        }

        return $result;
    }
}

    

Не ищите документ сразу после правки в редакторе

Компакция больше не пересчитывает поисковый индекс после правок в редакторе. Она только ставит документу IS_DERIVED_STALE='Y', а поиск и индекс ссылок пересчитывает агент DerivedProjectionAgent раз в 300 с (lib/Infrastructure/Agent/Freshness/DerivedProjectionAgent.php). Выключателя у агента нет. Код, который ищет документ сразу после правки в редакторе, найдёт его по старому тексту. Запись через REST note.document.update индекс по-прежнему обновляет сама.

Сверьте позиционные вызовы конструкторов

В нескольких командах Public\Command и в одном провайдере параметры сдвинулись:

  • SaveYjsStateCommand: после $yjsState вставлены bool $rebuiltFromMarkdown = false и ?string $markdownChecksum = null, в конец добавлен ?DocumentUpdateRepository;
  • OverwriteDocumentContentCommand: ?string $operationId стоит перед DocumentRepository $documentRepository;
  • RestoreDocumentVersionCommand: ?string $operationId стоит перед $versionRepository;
  • CompactDocumentCommand: параметр SearchIndexService $searchIndexService удалён, в конец добавлен EventRepository;
  • DeleteCollectionCommand: ?FavoriteRepository стоит перед $analyticsType;
  • Public\Provider\SubscriptionProvider::__construct: второй параметр SubscriptionRepository заменён на CoverageResolver.

Остальные изменённые команды расширили только в конце списка (чаще всего BacklinkLifecycleNotifier), их вызовы работают как раньше. Совместимо расширены и экшены синхронизации. CollaborationSyncController::saveYjsStateAction принимает $rebuiltFromMarkdown и $markdownChecksum, а savePatchAction добавляет в ответ поля patchId, prevPatchId, journalBaseId и compactSuggested. PushNotificationService::sendDocumentPatch(), sendDocumentContentOverwritten() и CollaborationProvider::buildCollaborationMeta() получили необязательные параметры в конце.

Новое

Работайте с избранным через команды

Избранным управляют экшены FavoriteController::addAction, removeAction, moveAction и listAction и команды AddFavoriteCommand, RemoveFavoriteCommand и MoveFavoriteCommand, а читать его можно через Public\Provider\FavoriteProvider::getPage() и getRow(). В выдаче CollectionController и Public\Provider\TreeProvider появился флаг isFavorite. Пару «избранное + подписка» сериализует FavoriteLockService, он берёт Connection::lock() на связку пользователя и объекта.

Включите обратные ссылки

Internal\Service\Link\DocumentLinkExtractor::extract() ищет в markdown ссылки на другие документы: токены @{document:<id>} и markdown-ссылки на /note/document/<id>/. Код и fenced-блоки он пропускает. Найденное DocumentLinkIndexService пишет в b_note_document_link. Если генерируете документы из своего кода и хотите, чтобы связи попали в индекс, ссылайтесь одним из этих двух способов.

Прочитать ссылки можно через Public\Provider\BacklinkProvider::getSources() и getCount() или экшенами DocumentController::getBacklinksAction и getBacklinksCountAction. Экшены работают только с включённым UI-флагом backlinks_enabled (по умолчанию N), иначе отвечают 403. Индекс для существующих документов строит разовый DocumentLinkBackfillAgent, а DocumentLinkRepairAgent запускается по требованию через DocumentLinkRepairScheduler.

Проекцию присылает клиент

CollaborationSyncController::materializeAction(int $documentId, string $markdown, int $uptoId) и команда Public\Command\MaterializeDocumentCommand принимают от клиента готовый markdown. Сервер записывает проекцию и курсор MATERIALIZED_UPTO_ID, только если присланный курсор опережает сохранённый. Журнал патчей, версии, события и UPDATED_AT при этом не меняются. Рядом работает Internal\Service\Collaboration\CollaborationEligibility. Документ, который перезапись перевела в обычный markdown, вернётся в совместный режим, только если клиент заявил rebuiltFromMarkdown и передал CRC-32 текста.

Проверьте условия бокового чата BitrixGPT

Конфигурацию виджета отдаёт AiChatController::getWidgetConfigAction(), action id note.infrastructure.AiChatController.getWidgetConfig. Доступность проверяет Internal\Integration\AiAssistant\AiChatAvailability, по порядку:

  1. флаг ai_chat_enabled (по умолчанию N);
  2. модули aiassistant и im;
  3. AiAssistantService::isBitrixGptV2Available('bitrixgpt_v2_available') и id бота;
  4. CopilotChat::isAvailable() && isActive().

WidgetApplicationDataResolver — обёртка над Bitrix\Im\V2\Application\Config. Сам диалог создаёт клиент через im.v2.Chat.add. Судя по комментариям, оба класса перенесли из biconnector и landing.

По мелочи

  • SubscriptionController::getStatesAction(int $collectionId, array $documentIds) отдаёт состояние подписок пачкой до 200 id, считает его Internal\Service\Subscription\CoverageResolver.
  • DocumentAccessService::buildListVisibilityFilter() строит предикат видимости как ConditionTree для ORM-выборок, раньше фильтрация шла в PHP.
  • CollectionAccessService кеширует коды доступа пользователя на время запроса, для тестов есть clearUserAccessCodesCache().
  • DocumentController::restoreVersionAction(..., $operationId = null) принимает идентификатор операции из 32 hex-символов и возвращает его в push-уведомлении, так вкладка узнаёт свою операцию.
  • UI-флаги backlinks_enabled и ai_chat_enabled читают новые Configuration::isBacklinksUiEnabled() и isAiChatEnabled(), оба флага по умолчанию выключены.

БД

Схема модуля описана в install/migrations/tables.php:

  • в b_note_document добавлены CONTENT_UPDATED_AT datetime без бэкфилла, MATERIALIZED_UPTO_ID bigint и IS_DERIVED_STALE char(1) default 'N' с индексом IX_NOTE_DOC_DERIVED_STALE (IS_DERIVED_STALE, ID);
  • в b_note_event новый индекс IX_NOTE_EVENT_TYPE (SCOPE, ENTITY_ID, EVENT_TYPE, ID);
  • новая таблица b_note_favorite с полями USER_ID, ENTITY_TYPE, ENTITY_ID, POSITION, CREATED_AT, уникальным индексом USER_ID + ENTITY_TYPE + ENTITY_ID и индексами для постраничной выборки и каскадной очистки;
  • новая таблица b_note_document_link с полями SOURCE_ID и TARGET_ID, уникальной парой и покрывающим индексом IX_NOTE_DOCLINK_TARGET (TARGET_ID, SOURCE_ID).

В install/migrations/agents.php добавлены DerivedProjectionAgent::run раз в 300 с и DocumentLinkBackfillAgent::run раз в 300 с. Второй снимает себя сам после полного прохода, а на установленных порталах его, по комментарию, регистрирует апдейтер.

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

ConditionTree::whereIn() с пустым массивом ничего не добавляет в фильтр (bitrix/modules/main/lib/ORM/Query/Filter/ConditionTree.php, строки 277–285). Докблок buildListVisibilityFilter() предупреждает, что без кодов доступа такой фильтр превратился бы в «всё, что кому-либо выдано». Поэтому модуль добавляет ветки только при непустом входе, а без веток возвращает заведомо ложное where($documentField, '<', 0).

Bitrix\Main\Command\AbstractCommand::run() заворачивает любое исключение из execute() в CommandException. Из-за этого catch (DocumentArchivedException) вокруг run() в CollaborationSyncController версии 26.300 не срабатывал ни разу, и клиент получал общую ошибку. Теперь addSyncError() достаёт причину через getPrevious().

На MySQL 5.6 сессия держит только одну именованную блокировку, и второй GET_LOCK молча отпускает первую. Connection::lock() в ядре устроен как раз через GET_LOCK (bitrix/modules/main/lib/db/mysqlcommonconnection.php:297). DerivedProjectionAgent это учитывает и берёт блокировку только на захват пачки. Очередью служит сам флаг IS_DERIVED_STALE, и агент сначала сбрасывает его, а потом переиндексирует документ.

Вложенный rollback в ядре откатывается к savepoint и бросает TransactionException('Nested rollbacks are unsupported.') (rollbackTransaction() в том же mysqlcommonconnection.php). Поэтому при отказе SetSubscriptionCommand коммитит пустую транзакцию вместо отката. В FavoriteLockService расписано, почему SELECT под REPEATABLE READ инвариант не защищает и нужна именованная блокировка.

CopilotChat::checkCopilotAvailability() модуль не вызывает намеренно. Через AIHelper этот метод регистрирует бота Copilot, если его нет, то есть пишет в БД прямо при рендере страницы (lib/Internal/Integration/AiAssistant/AiChatAvailability.php).

У параметров экшенов $rebuiltFromMarkdown, $markdownChecksum и $operationId тип не указан. Из urlencoded-запроса false приходит строкой "false", а operationId[]=x приходит массивом.

В выпуске есть заготовки без кода. Lang-файлы импорта из «Базы знаний 1.0» на Сайтах (lang/en/lib/Internal/Service/Import/Source/LandingSource.php, Transformer/Landing/HtmlToMarkdown.php, MenuConverter.php) и lang-файлы REST-исключений TreeTooLargeException и MoveAccessEscalationException уже лежат, самих классов в Infrastructure\Rest\V3\Exceptions нет (внутренний Internal\Exceptions\MoveAccessEscalationException есть с 26.300). CompactDocumentCommand по-прежнему называет RAG потребителем onDocumentContentSettled, но подписчика в дистрибутиве нет.

MATERIALIZED_UPTO_ID сразу сделали bigint. Курсор движется только вперёд, и усечённое значение навсегда заморозило бы MARKDOWN.

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

note 26.300.0 Заметное Свежее

note 26.300.0: у Базы знаний 2.0 появились события, история версий и подписки

Модуль note в коробочном Битрикс24 отвечает за «Базу знаний 2.0»: коллекции с деревом markdown-документов, совместное редактирование на Yjs, свои права доступа и REST v3 `note.*`. В 26.300.0 он получил первые публичные события, историю версий, подписки с уведомлениями в IM, массовые операции над дер...

4 мин
voximplant 26.800.0 Ломающее Свежее

Voximplant 26.800.0: attachRecord и finish отказывают локальным адресам, searchCrmEntities отдаёт один лид

В voximplant 26.800.0 у четырёх REST-методов внешней телефонии поменялось поведение, хотя сигнатуры остались прежними. `telephony.externalCall.attachRecord` и `telephony.externalCall.finish` с `RECORD_URL` больше не скачивают записи с приватных адресов и не ходят через прокси. `searchCrmEntities` от...

4 мин
timeman 26.100.0 Ломающее Свежее

Timeman 26.100.0: время считается по IANA-зоне сотрудника, у CTimeManEntry и REST другие значения

С timeman 26.100.0 учёт рабочего времени берёт зону сотрудника из IANA-идентификатора в `b_user.TIME_ZONE` и считает смещение UTC на момент каждого события, с переходами на летнее и зимнее время. Форматы полей и ответов остались прежними, поменялись значения: `TIME_START`, `TIME_FINISH` и `DURATION`...

3 мин
report 26.200.100 Безопасность Свежее

Report 26.200.100: закрыта SQL-инъекция через формат имени пользователя

Релиз «Конструктора отчётов» вендор описал фразой «Усилена безопасность модуля». На деле изменилась одна строка в `CReport::getFormattedNameExpr()`, теперь метод экранирует литералы из формата имени пользователя перед вставкой в SQL. Если модуль `report` установлен, обновите его до 26.200.100. Файл...

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

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

на связи

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

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

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

Войти