feat: триггер schedule для автоматизаций — ежедневный запуск по МСК

- планировщик server/workers/automation-scheduler.ts (тик 60 сек, все организации)
- защита от дублей: automations.last_scheduled_run_at (миграция 0065), catch-up в пределах суток
- UI: выбор триггера 'По расписанию' и времени запуска в Automations.tsx
- MCP: create_automation/update_automation принимают trigger 'schedule'
- triggerData: { trigger: 'schedule', scheduledAt }
This commit is contained in:
2026-07-24 13:52:03 +03:00
parent c152a670ae
commit b4bded2ffb
8 changed files with 130 additions and 7 deletions

View File

@@ -38,6 +38,7 @@ const TRIGGER_LABELS: Record<string, string> = {
'task.created': 'Задача создана',
'task.status_changed': 'Статус изменён',
'qr.scan': 'Сканирование QR',
schedule: 'По расписанию',
};
const CODE_TEMPLATE = `// ============================================================
@@ -54,6 +55,7 @@ const CODE_TEMPLATE = `// ======================================================
// • task.created: { formId, taskId, task: { ... } }
// • task.status_changed:{ formId, taskId, oldStatusId, newStatusId, task: { ... } }
// • qr.scan: { template, objectId, scannedByUserId }
// • schedule: { trigger: 'schedule', scheduledAt }
//
// Логирование:
// ctx.log(msg) — запись в лог выполнения
@@ -86,6 +88,7 @@ const CODE_TEMPLATE = `// ======================================================
// task.created — после создания задачи
// task.status_changed — при изменении статуса задачи
// qr.scan — при сканировании QR-кода в PWA (только офлайн-автоматизации)
// schedule — ежедневно в заданное время по МСК (triggerConfig: { time: 'HH:MM' })
//
// Результат автоматизации (ctx.result):
// { allow: false, error: '...' } — отменить действие (task.before_create)
@@ -123,6 +126,7 @@ export function AutomationsContent() {
const [formCode, setFormCode] = useState(CODE_TEMPLATE);
const [formTrigger, setFormTrigger] = useState('manual');
const [formTriggerFormId, setFormTriggerFormId] = useState('');
const [formTriggerTime, setFormTriggerTime] = useState('08:00');
const [formActive, setFormActive] = useState(true);
const [formRunOffline, setFormRunOffline] = useState(false);
const [formClientCompatible, setFormClientCompatible] = useState(false);
@@ -186,6 +190,7 @@ export function AutomationsContent() {
setFormCode(CODE_TEMPLATE);
setFormTrigger('manual');
setFormTriggerFormId('all');
setFormTriggerTime('08:00');
setFormActive(true);
setFormRunOffline(false);
setFormClientCompatible(false);
@@ -199,6 +204,7 @@ export function AutomationsContent() {
setFormCode(item.code);
setFormTrigger(item.trigger);
setFormTriggerFormId(String((item.triggerConfig as any)?.formId ?? 'all'));
setFormTriggerTime(String((item.triggerConfig as any)?.time ?? '08:00'));
setFormActive(item.isActive);
setFormRunOffline(item.runOffline ?? false);
setFormClientCompatible(item.clientCompatible ?? false);
@@ -212,7 +218,9 @@ export function AutomationsContent() {
function handleSave() {
if (!formName.trim()) return;
const triggerConfig: Record<string, unknown> = {};
if (formTrigger !== 'manual' && formTriggerFormId && formTriggerFormId !== 'all') {
if (formTrigger === 'schedule') {
triggerConfig.time = formTriggerTime || '08:00';
} else if (formTrigger !== 'manual' && formTriggerFormId && formTriggerFormId !== 'all') {
triggerConfig.formId = parseInt(formTriggerFormId);
}
// For qr.scan trigger, object matching uses triggerConfig.templateKey
@@ -282,6 +290,7 @@ export function AutomationsContent() {
task.created: { formId, taskId, task: { ... } }
task.status_changed:{ formId, taskId, oldStatusId, newStatusId, task: { ... } }
qr.scan: { template, objectId, scannedByUserId }
schedule: { trigger: 'schedule', scheduledAt }
Формы:
ctx.forms.list() / ctx.forms.get(formId) / ctx.forms.getFields(formId) / ctx.forms.getStatuses(formId)
@@ -305,6 +314,7 @@ export function AutomationsContent() {
task.created — после создания задачи
task.status_changed — при изменении статуса задачи
qr.scan — при сканировании QR-кода в PWA
schedule — ежедневно в заданное время по МСК (настройка: время запуска)
Результат автоматизации (ctx.result):
{ allow: false, error: '...' } — отменить действие (task.before_create)
@@ -444,12 +454,25 @@ export function AutomationsContent() {
<SelectItem value="task.created">При создании задачи</SelectItem>
<SelectItem value="task.status_changed">При изменении статуса</SelectItem>
<SelectItem value="qr.scan">Сканирование QR</SelectItem>
<SelectItem value="schedule">По расписанию (ежедневно, МСК)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{formTrigger !== 'manual' && (
{formTrigger === 'schedule' && (
<div className="space-y-1">
<Label className="text-xs">Время запуска (МСК)</Label>
<Input
type="time"
className="h-7 text-xs w-auto"
value={formTriggerTime}
onChange={e => setFormTriggerTime(e.target.value)}
/>
</div>
)}
{formTrigger !== 'manual' && formTrigger !== 'schedule' && (
<div className="space-y-1">
<Label className="text-xs">Форма (опционально)</Label>
<Select value={formTriggerFormId} onValueChange={setFormTriggerFormId}>