2. Руководство разработчика › 2.7 Эксплуатация › Отслеживание событий

Событие — запись в журнале магазина о том, что что-то случилось: сохранены цены, пришёл заказ, наступило утро рабочего дня. Пишут события движок и модули, читают — Диспетчер программы, модули и AI-диспетчер. Как события выглядят для сотрудника — «События» в руководстве пользователя; здесь — как они устроены и как завести свои.

Журнал

Журнал ведёт сервер магазина, один для всех рабочих мест. Строка журнала:

Поле Что в нём
id номер события, растёт с каждой записью
user_id автор — пользователь, который записал событие; пусто — сам магазин. У события операции — тот, кто её выполнил
princ_id принципал — пользователь, чьими правами AI-диспетчер выполнит команды AI-инструментов, которых ему не выдано; пусто — принципала нет
key_name ключ — код события, например kNewOrder
state состояние — слово, о котором договаривается магазин; по умолчанию ALONE
data данные события в JSON
for_users, for_groups кому адресовано: номера пользователей и групп через запятую; пустые — всем
date_time время записи по часам магазина

Модуль читает журнал дверями «Журнала событий», агент — AI-инструментом магазина, построенным на тех же дверях.

Диспетчеру программы приходят только номер, ключ и время события. Данные читает тот, кто событие разбирает, — по номеру:

$event = MELBIS()->SysUserEventLoad($event_id);
$order_id = $event['data']['order_id'] ?? 0;

События операций

У любой операции в «Каталоге операций» есть «Ключ события» и «Состояние» (окно «Пользователи и права доступа»). Пустой ключ — событие не пишется; пустое состояние — событие ложится с ALONE.

Событие пишется один раз на действие сотрудника, после того как операция выполнена:

Действие Когда пишется событие
команда к серверу после успешного выполнения
сохранение окна с блокировкой когда сохранение дошло до конца
сохранение окна без блокировки, слияние в «Описаниях», импорт в «Прайс-листах» один раз, после всех порций
открытие окна операцией получения данных, при первой порции
данные, которые программа получает порциями на первой порции
инструмент AI-помощника по операции его права — например, «AI-помощник → Инструменты → Выполнить»; отказ события не пишет

Кто выполнил операцию, событие записывает автором, в user_id, а данные говорят, что выполнено:

{"command": "UPDATE_PRICE_STORE"}

command — «Команда для сервера» операции. События операций адресованы всем.

Ключ у операции «Бизнес → Диспетчер → Проверка уведомлений» ничего не даёт: это ожидание, а не действие сотрудника.

События модулей

Модуль пишет событие дверью SysUserEventAdd. Ставят её туда, где случается сам факт, — в общую логику, через которую идут все пути.

Так устроен kNewOrder демо-магазина. Его пишет LOGIC_ORDER_EDIT\Run — функция, которая записывает каждую версию заказа: через неё проходят и корзина на витрине, и окно «Заказы» программы, и AI-инструменты. Новый заказ отличается от новой версии старого тем, что строки заказа ещё нет:

$order = MELBIS()->SqlSelectFlat(__LINE__, $command, $param);
$order_new = !isset($order['id']);
if ( $order_new )
{
    // The order is born here
}

// ...

// Tell the store about a new order, wherever it was placed
if ( $order_new )
{
    $data_event = [
        'order_id' => $order['id']
        ];
    MELBIS()->SysUserEventAdd(null, null, 'kNewOrder', 'ALONE', $data_event);
}

Событие пишется после всех записей заказа, поэтому тот, кто его разберёт, найдёт заказ целиком.

Ключ называют как ключи «Реестра настроек»: k и слово с большой буквы — kNewOrder, kPricesSaved.

Данные — номера, а не копии строк: заказ, товар, покупатель. Что с ними сейчас, разборщик прочтёт сам.

Получатели — списки номеров пользователей и групп; пустые — всем.

Автор — номер пользователя, от которого событие; null — сам магазин.

Принципал — номер пользователя, если событие поручает AI-диспетчеру работу его правами. Права передаёт только событие, адресованное диспетчеру по номеру или по группе. Подпись двери и пример — «Журнал событий».

Событие пишет и задача по расписанию. Модуль, который «Планировщик задач» вызывает раз в день, отмечает утро рабочего дня:

namespace MELBIS_CRON_MORNING;

function Main($mVars)
{
    header('Content-Type: text/plain; charset=utf-8');
    MELBIS()->CronLocalOnly();

    // The morning of a working day, for whoever makes the summary
    $day = MELBIS()->DateTime('now', 'Y-m-d');
    $data_event = [
        'day' => $day
        ];
    $event_id = MELBIS()->SysUserEventAdd(null, null, 'kMorning', 'ALONE', $data_event);
    echo "Event $event_id\r\n";

    return '';
}

Доставка до программы

Диспетчер каждой программы держит у сервера один долгий запрос. Сервер ждёт до 25 секунд и отвечает сразу, как только изменился журнал событий или чат; в тишине — по истечении срока, и программа тут же задаёт следующий. Событие модуля будит запрос так же, как событие операции.

Диспетчер получает события, адресованные всем, этому пользователю или его группе.

Программа помнит номер последнего увиденного события и спрашивает события после него. Сразу после запуска она ставит метку на последнее событие журнала и ничего по нему не запускает, поэтому события, случившиеся при закрытой программе, уведомлений не вызывают.

Подписка. Чтобы Диспетчер отзывался на событие, его ключ должен стоять в «Реестре настроек»:

На каждом рабочем месте задают своё:

Команда запускается свёрнутой, в папке магазина, и получает событие в переменных окружения:

Переменная Что в ней
MELBIS_EVENT ключ события
MELBIS_ID номер события; у кнопки «Тест» — 0
MELBIS_TIME время события

Готовые команды агента — claude.bat, codex.bat, opencode.bat — переменные не читают. Номер события MCP-сервер агента получает от программы вместе с логином, а само событие агент видит в ответе на вход в магазин. Номер 0 — запуск кнопкой «Тест»: события нет, это тестовый запуск.

Разбор модулем

Событие разбирают и без Диспетчера — модулем по расписанию, который ищет события своего ключа. Чтобы два разборщика не взяли одно событие, его берут сменой состояния с условием на прежнее: из двух одновременных попыток ACCEPT получит одна. Взятое событие после работы отмечают сделанным. Начать чтение с какого-то времени помогает SysUserEventLast с $mBeforeTime: номер последнего события до этого времени — курсор для SysUserEventList.

Журнал растёт с каждым событием, и убирает в нём тот, кто ведёт разбор, — например, задача по расписанию раз в сутки удаляет сделанное старше недели. Метку Диспетчера уборка не сбивает.

Подписи дверей, слова ответа (ACCEPT, ABSENT, TAKEN), цикл разбора и уборка с примерами — «Журнал событий».

Каскад событий

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

Например, из всех новых заказов агенту нужны только крупные:

kNewOrder ── модуль раз в минуту ──> kOrderAttention ──> AI-диспетчер
             сумма ниже порога: отметил и забыл

Модуль берёт события kNewOrder, которые ещё никто не разбирал, проверяет сумму и на крупный заказ пишет kOrderAttention с номером заказа и причиной:

namespace MELBIS_CRON_ORDER_WATCH;

// The sum from which an order wants the AI-dispatcher
const SUM_ATTENTION = 1000;

function Main($mVars)
{
    header('Content-Type: text/plain; charset=utf-8');
    MELBIS()->CronLocalOnly();

    // New orders nobody has looked at yet
    $event_set = MELBIS()->SysUserEventList(0, 'kNewOrder', 'ALONE');
    foreach ( $event_set as $event )
    {
        // Two runs never take the same event
        $take = MELBIS()->SysUserEventState($event['id'], 'LAUNCH', 'ALONE');
        if ( $take != 'ACCEPT' ) continue;

        // Only an order that needs a look goes further
        $order_id = $event['data']['order_id'] ?? 0;
        $order_sum = OrderSum($order_id);
        if ( $order_sum >= SUM_ATTENTION )
        {
            $data_event = [
                'order_id' => $order_id,
                'reason'   => 'sum'
                ];
            MELBIS()->SysUserEventAdd(null, null, 'kOrderAttention', 'ALONE', $data_event);
        }

        MELBIS()->SysUserEventState($event['id'], 'DONE');
        echo "Order $order_id: $order_sum\r\n";
    }

    return '';
}

/**
 * OrderSum
 **/
function OrderSum($mOrderId)
{
    $command = "SELECT order_version.total_sum
                  FROM {DBNICK}_orders orders
                  JOIN {DBNICK}_orders_version order_version
                    ON order_version.id = orders.version_id
                 WHERE orders.id = :ORDER_ID
               ";
    $param_order = [
        'order_id' => $mOrderId
        ];
    $order = MELBIS()->SqlSelectFlat(__LINE__, $command, $param_order);

    return (float) ( $order['total_sum'] ?? 0 );
}

Задача объявляется в cron.php раз в минуту:

MELBIS()->CronAdd('* * * * *', 'http://localhost/?mod=melbis_cron_order_watch', 60, 'order_watch');

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

Что даёт каскад:

Задержка каскада — шаг расписания: при запуске раз в минуту событие доходит до агента не позже чем через минуту. Состояние у события одно, поэтому по состояниям его разбирает один модуль; остальные читают это событие, не меняя состояния.

Как завести своё событие

На операцию программы.

  1. Задайте «Ключ события» у операции в «Каталоге операций».
  2. Добавьте ключ в «Список событий» «Реестра настроек».
  3. Отметьте событие в Диспетчере тех рабочих мест, которые должны отзываться.

В модуле.

  1. Поставьте SysUserEventAdd туда, где случается факт, — в общую логику, а не в одну из её дверей.
  2. Добавьте ключ в «Список событий».

По расписанию.

  1. Модуль задачи пишет событие.
  2. Строка в cron.php задаёт, когда его вызвать.

Для AI-диспетчера.

  1. Заведите событие одним из способов выше.
  2. Дайте агенту заметку на этот ключ и подпишите его Диспетчер — см. «AI-диспетчер».
  3. Если событие частое, поставьте между ним и агентом каскад.
  4. Если работа должна идти правами пользователя, модуль записывает его номер в событие как принципала — «Поручения с правами пользователя».

На что обратить внимание