Руководство Melbis Shop: оглавление
Событие — запись в журнале магазина о том, что что-то случилось: сохранены цены, пришёл заказ, наступило утро рабочего дня. Пишут события движок и модули, читают — Диспетчер программы, модули и 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 говорит, что делать по каждой причине.
Новое правило — новая причина в модуле и строка в заметке, а не новая
подписка.
Что даёт каскад:
Задержка каскада — шаг расписания: при запуске раз в минуту событие доходит до агента не позже чем через минуту. Состояние у события одно, поэтому по состояниям его разбирает один модуль; остальные читают это событие, не меняя состояния.
На операцию программы.
В модуле.
SysUserEventAdd туда, где случается факт, — в
общую логику, а не в одну из её дверей.По расписанию.
cron.php задаёт, когда его вызвать.Для AI-диспетчера.
princ_id
записан: иначе его права получил бы каждый, у чьего Диспетчера есть
команда.