note 26.300.0 Заметное

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

4 мин чтения

Модуль note в коробочном Битрикс24 отвечает за «Базу знаний 2.0»: коллекции с деревом markdown-документов, совместное редактирование на Yjs, свои права доступа и REST v3 note.*. В 26.300.0 он получил первые публичные события, историю версий, подписки с уведомлениями в IM, массовые операции над деревом, наследование прав по поддереву и выгрузку документа в ZIP. Дифф весит около 7 МБ, и почти всё в нём занимают JS-бандлы, один editor.bundle.js изменился на 28 665 строк (+16 332 / −12 333).

Ваш код релиз заденет, если вы создаёте команды модуля напрямую или ходите в его AJAX и REST из интеграций. Пути ниже указаны от bitrix/modules/note/.

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

Передавайте параметры команд по имени

В конструкторы нескольких команд Bitrix\Note\Public\Command новые параметры вставили в середину списка, и позиционные вызовы съедут:

  • CreateDocumentCommand: на девятую позицию, перед bool $notifyInitiator, встал ?DocumentRepository $repository, а перед string $analyticsType добавлены ?DomainEventPublisher и ?EventLogService;
  • ArchiveDocumentCommand и DeleteDocumentCommand: перед $analyticsType появились $eventPublisher, $eventLogService, bool $withNested = true и $reparentService;
  • ArchiveCollectionCommand: перед $analyticsType стоит ?DomainEventPublisher;
  • DeleteCollectionCommand: перед $analyticsType стоят $eventPublisher, $documentRepository и $hardDeleteService.

MoveDocumentCommand, UpdateDocumentCommand, RestoreDocumentCommand, CompactDocumentCommand, OverwriteDocumentContentCommand, HardDeleteDocumentCommand и Restore*FromRecycleBinCommand расширили только в конце списка, их вызовы работают как раньше. Если собираете команды сами, передавайте хвост по имени (analyticsType: '…', notifyInitiator: false), тогда следующая вставка в середину вас не заденет.

Проверьте интеграции на тарифе без Базы знаний

В 26.250 тариф проверяли только компоненты note.editor и note.config.permissions и мобильный коннектор. Теперь проверка стоит на всех ajax-экшенах модуля (фильтр NoteAccess), на REST v3 (NoteRestAccess), на ajax компонента прав, в EditorUploaderController и в провайдерах entity-selector. Доступ закрывается, если тарифная опция недоступна или выключен инструмент menu_note_base, а решение принимает Internal\Service\License\LicenseService::isAccessBlocked().

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

Читайте описание коллекции из markdownDescription

У коллекции появился главный документ, в нём хранится описание базы знаний. REST v3 такой документ прячет. note.document.get, update, archive и delete отвечают на него EntityNotFoundException, как на несуществующий, а add с главным документом в parentId бросает InvalidParentException. Описание коллекции REST v3 отдаёт в read-only поле markdownDescription. Команды ArchiveDocumentCommand и DeleteDocumentCommand на главный документ бросают MainDocumentOperationException.

Замените deleteUpToId()

Из Internal\Repository\DocumentUpdateRepository удалён deleteUpToId(). Вместо него deleteUpToIdReturningAuthors(int $documentId, int $maxId), который возвращает ещё и авторов вычищенных патчей. Класс лежит в Internal, к публичному API модуль его не относит, но если вы его всё-таки вызывали, код упадёт.

Новое

Подпишитесь на события документов

Это первые публичные события модуля, до 26.300.0 своему коду в note зацепиться было не за что.

note:onDocumentLifecycle (класс Public\Event\OnDocumentLifecycleEvent) приходит с параметрами:

  • reason — created, archived, deleted, hardDeleted, restored, collectionDeleted или moved;
  • collectionId;
  • documentIds — весь каскад приходит одним событием;
  • from и to, только для moved.

note:onDocumentContentSettled (Public\Event\OnDocumentContentSettledEvent) несёт reason (compacted, overwritten или titleChanged), collectionId и documentId.

Оба события отправляет Internal\Service\DomainEventPublisher через Application::addBackgroundJob(), уже после ответа пользователю. Событие может прийти и после откатившейся транзакции, то есть рассказать о том, чего в базе нет. Поэтому remove() и reindex() в примере ниже должны быть идемпотентными, а reindex() пусть сам перечитывает документ и убирает его из индекса, если документа уже нет.

Регистрация в install/index.php своего модуля:

        <?php declare(strict_types=1);

use Bitrix\Main\EventManager;
use Vendor\KbSync\Integration\Note\DocumentEventHandler;

// DoInstall(); в DoUninstall() те же вызовы через unRegisterEventHandler()
$events = EventManager::getInstance();
$events->registerEventHandler('note', 'onDocumentLifecycle', 'vendor.kbsync', DocumentEventHandler::class, 'onLifecycle');
$events->registerEventHandler('note', 'onDocumentContentSettled', 'vendor.kbsync', DocumentEventHandler::class, 'onContentSettled');

    

Сам обработчик. DocumentIndex здесь ваш сервис, зарегистрированный в .settings.php модуля:

        <?php declare(strict_types=1);

namespace Vendor\KbSync\Integration\Note;

use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Event;
use Vendor\KbSync\Search\DocumentIndex;

final class DocumentEventHandler
{
    public static function onLifecycle(Event $event): void
    {
        $index = self::index();
        $apply = match ((string) $event->getParameter('reason')) {
            'archived', 'deleted', 'hardDeleted', 'collectionDeleted' => $index->remove(...),
            'created', 'restored', 'moved' => $index->reindex(...),
            default => null,
        };
        if ($apply === null) {
            return;
        }

        $documentIds = array_unique(array_map('intval', (array) $event->getParameter('documentIds')));
        foreach ($documentIds as $documentId) {
            $apply($documentId);
        }
    }

    public static function onContentSettled(Event $event): void
    {
        self::index()->reindex((int) $event->getParameter('documentId'));
    }

    private static function index(): DocumentIndex
    {
        return ServiceLocator::getInstance()->get(DocumentIndex::class);
    }
}

    

Покажите историю версий

История хранится в новых таблицах с ORM-классами EventTable, EventAuthorTable, DocumentVersionTable и DocumentViewTable, а записывает её EventLogService::record(). Читать её из PHP можно через Public\Provider\FeedProvider::listByDocument(), VersionProvider::getVersionBody() и ViewProvider::getViews(), откатывать версию командой RestoreDocumentVersionCommand. Фронтенду те же операции отдают экшены DocumentController: listFeedAction, getVersionAction, restoreVersionAction и getViewsAction.

VersionCleanupAgent удаляет версии старше version_ttl_days дней (по умолчанию 90) и вместе с ними файлы, на которые после этого никто не ссылается.

Настройте подписки и уведомления в IM

Подписки хранит SubscriptionTable. На документ бывают режимы self, subtree и muted, на коллекцию режим all. Управляют ими экшены SubscriptionController::getStateAction, setAction и removeAction, команды SetSubscriptionCommand и RemoveSubscriptionCommand, читать их можно через Public\Provider\SubscriptionProvider.

Доставкой занимается NotificationDrainAgent. Раз в 300 с он читает b_note_event и через Internal\Integration\Im\ImNotificationGateway вызывает CIMNotify::Add. Если в пачке у одного получателя набралось больше одного документа, он получит одно сводное уведомление. Свои типы в настройках IM модуль объявляет обработчиком NotifySchemaHandler::onGetNotifySchema(). По умолчанию у них включён канал SITE, а MAIL и PUSH выключены. Размер пачки задаёт опция notify_batch_size (по умолчанию 500). Опция notify_interval в Configuration объявлена, но её геттер никто не вызывает, а интервал агента задан при регистрации (300 с).

Архивируйте и переносите пачкой

Массовые команды построены на AbstractBulkCommand: Bulk*DocumentsCommand для архива, удаления, удаления насовсем, переноса и восстановления, плюс ArchiveAllInCollectionCommand и DeleteAllInCollectionCommand. Для фронтенда есть экшены archiveManyAction, deleteManyAction, moveManyAction, restoreManyAction и resolveBulkSelectionAction, а для корзины RecycleBinController::hardDeleteManyAction и restoreManyAction. Итог операции приходит объектом Internal\Service\Bulk\BulkOutcome.

После раскрытия поддеревьев в выборке может оказаться не больше bulk_selection_limit документов, по умолчанию 1000. Лимит проверяет SelectionResolver.

Выдавайте права сразу на поддерево

Internal\Service\Access\SubtreeAclReconciler раскладывает грант на поддерево в производные строки b_note_document_access с заполненным SOURCE_DOCUMENT_ID. Если строк выходит больше 500 (узлы × субъекты), расширение прав уходит в агент SubtreeAclReconcileAgent, который регистрирует себя сам.

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

Выгружайте документ в ZIP

Выгрузка идёт в два запроса. ExportController::prepareZipAction(int $documentId, string $content) возвращает токен TimeSigner на 15 минут, с солью note.export.zip и id пользователя в подписи. downloadZipAction(string $token) отдаёт архив без проверки CSRF, на чужой токен отвечает 404, на просроченный 410.

Архив собирает DocumentZipExportService. В него попадает не больше 100 вложений общим объёмом до 256 МБ, а готовый пакет лежит в CTempFile два часа.

Включите нужные флаги

Опция activity_enabled включена по умолчанию, а UI-флаги history_enabled, notifications_enabled, hotkeys_enabled и markdown_io_enabled выключены, как и subtree_inheritance_enabled. Включаются они обычными опциями модуля note:

        <?php declare(strict_types=1);

use Bitrix\Main\Config\Option;

Option::set('note', 'history_enabled', 'Y');             // история версий в интерфейсе
Option::set('note', 'notifications_enabled', 'Y');       // подписки в интерфейсе
Option::set('note', 'subtree_inheritance_enabled', 'Y'); // новые гранты на поддерево
Option::set('note', 'version_ttl_days', '30');           // версии живут 30 дней вместо 90

    

По мелочи

  • Главный документ коллекции создаёт MainDocumentService::ensureMainDocument(), для старых коллекций есть агент MainDocumentBackfillAgent. Судя по его докблоку, агента регистрирует апдейтер.
  • DocumentController::archiveAction и deleteAction получили bool $withNested = true. По умолчанию каскад работает как раньше, а с false дочерние документы переезжают к родителю через ReparentService. createAction теперь принимает string $markdown = ''.
  • listAccessibleTreeAction и listAccessibleChildrenAction строят дерево документов, доступных пользователю (Internal\Service\Sidebar\AccessibleTreeService).
  • OrphanFileCleanupAgent за один проход чистит файлы, которые осиротели ещё до появления версий.
  • У PushNotificationService появились sendToUserChannels() и notifyDocumentGrantees().
  • В AccessController::check(), loadItem() и BaseRule::execute() параметры со значением null по умолчанию стали явно nullable.

БД

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

  • в b_note_document добавлены поле IS_MAIN char(1) default 'N' и индекс IX_NOTE_DOC_MAIN (COLLECTION_ID, IS_MAIN);
  • в b_note_document_access добавлены поле SOURCE_DOCUMENT_ID int not null default 0 (ноль у явного гранта) и индекс IX_NOTE_DOC_ACCESS_SOURCE;
  • уникальный индекс IX_NOTE_DOCUMENT_ACCESS_UNIQ (DOCUMENT_ID, SUBJECT_CODE) заменён на IX_NOTE_DOC_ACCESS_SRC_UNIQ (DOCUMENT_ID, SUBJECT_CODE, SOURCE_DOCUMENT_ID), на установленных порталах замену по комментарию делает апдейтер;
  • новые таблицы: b_note_event (bigint ID, SCOPE, ENTITY_ID, EVENT_TYPE, USER_ID, VERSION_ID, CREATED_AT и индексы по ленте), b_note_event_author, b_note_document_version (MARKDOWN в mediumtext, индекс по CREATED_AT для TTL), b_note_document_view (первичный ключ DOCUMENT_ID + USER_ID) и b_note_subscription (уникальный индекс USER_ID + SCOPE + ENTITY_ID).

Если читаете b_note_document_access своими запросами, учтите, что на пару «документ + субъект» теперь может прийтись несколько строк: явный грант и производные от разных поддеревьев.

В install/migrations/agents.php зарегистрированы VersionCleanupAgent::run раз в 7200 с и NotificationDrainAgent::run раз в 300 с, SubtreeAclReconcileAgent там намеренно нет. В install/migrations/events.php добавлен обработчик im:OnGetNotifySchema.

Поля TITLE и MARKDOWN в DocumentTable и NAME в CollectionTable получили модификаторы Emoji::encode на запись и Emoji::decode на чтение. По комментарию, соединение MySQL в utf8mb3 молча обрезает четырёхбайтовые эмодзи. Запись в эти поля мимо ORM модификаторы обойдёт, а прямое чтение вернёт эмодзи в виде :f09f9880:.

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

Экспорт в ZIP обходит четыре особенности ядрового CZip, и все четыре видны в bitrix/modules/main/classes/general/zip.php:

  • файлам проставляются external attributes 0xFE49FFE0, и распаковщик мобильного приложения показывает все записи папками, поэтому модуль после упаковки патчит central directory прямо в байтах;
  • имена пишутся в CP866 без UTF-8-флага, кириллица в сторонних распаковщиках превращается в кракозябры, и модуль транслитерирует имена;
  • ZIP_START_TIME задаётся через define() один раз на процесс, так что в долгоживущем процессе каждый следующий Pack() сразу уходит в ветку продолжения, и модуль ставит бюджет шага 86 400 с;
  • _openFile() при ошибке возвращает массив ошибок, а проверка if (!$this->_openFile("wb")) принимает его за успех, поэтому целевой файл модуль создаёт заранее через Main\IO.

Уведомление content_changed отправляется без NOTIFY_TAG. С непустым тегом CIMMessenger::Add перезаписывает PARAMS['USERS'] набором для схлопывания уведомлений и теряет настоящий список соавторов (lib/Internal/Integration/Im/ImNotificationGateway.php).

Отдельного outbox у уведомлений нет, очередью служит b_note_event. Курсор NotificationDrainAgent хранит в опции event_notify_last_id и сдвигает только после доставки всей пачки. Пересечение тиков исключает Connection::lock('note_notify_drain'). Через 10 секунд watchdog перестаёт брать новые пачки, а начатую агент дорабатывает. Если хвост остался, агент ставит global $pPERIOD = 1 и дожимает его на следующем хите. События старше суток не рассылаются, иначе ленивый бэкфилл created с историческими датами завалил бы подписчиков уведомлениями.

SubtreeAclReconcileAgent держит всё состояние в NAME агента, run(sourceId, collectionId, cursor). Тик возвращает выражение с новым курсором, пустая строка снимает агента. AddAgent дедуплицирует агентов по NAME, а $existError = false не даёт засорить $APPLICATION исключением CAdminException.

DomainEventPublisher в комментарии называет потребителем событий идемпотентный «RAG sync subscriber», но подписчиков на onDocumentLifecycle и onDocumentContentSettled в дистрибутиве нет.

Появился lang/en/lib/Infrastructure/Controller/AiChatController.php с фразой «Chat is not available». Самого контроллера в этой версии нет, он приходит в note 26.400.0.

Опцию phase4_broadcast_enabled оставили с номером фазы в имени, потому что переименование молча сбросило бы переключатель на порталах. В комментариях кода встречаются ссылки на внутреннюю разметку спецификаций: [P7.T1 / ALG-03 NORMATIVE], [PRD §6.2], SDD P7.

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

note 26.400.0 Ломающее Свежее

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

В [note 26.300.0](/bitrix-updates/note/note-26-300-0) модуль «Базы знаний 2.0» получил события, историю версий и подписки. Версия 26.400.0 добавляет избранное, обратные ссылки между документами, отдельную материализацию markdown-проекции Yjs-документа, асинхронный пересчёт поиска и боковой чат Bitri...

2 мин
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 Домовому.

Войти