From b4bded2ffb66d1bfbb93eb4cbba99e992065a270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Fri, 24 Jul 2026 13:52:03 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=82=D1=80=D0=B8=D0=B3=D0=B3=D0=B5?= =?UTF-8?q?=D1=80=20schedule=20=D0=B4=D0=BB=D1=8F=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B9=20?= =?UTF-8?q?=E2=80=94=20=D0=B5=D0=B6=D0=B5=D0=B4=D0=BD=D0=B5=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=9C=D0=A1=D0=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - планировщик 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 } --- client/src/pages/Automations.tsx | 27 ++++++- .../0065_automations_last_scheduled_run.sql | 2 + server/index.ts | 5 ++ server/mcp.ts | 10 ++- server/routes/automation.routes.ts | 2 +- server/storage/content.storage.ts | 13 ++++ server/workers/automation-scheduler.ts | 77 +++++++++++++++++++ shared/schema.ts | 1 + 8 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 migrations/0065_automations_last_scheduled_run.sql create mode 100644 server/workers/automation-scheduler.ts diff --git a/client/src/pages/Automations.tsx b/client/src/pages/Automations.tsx index eb1f5d1..d61b0b7 100644 --- a/client/src/pages/Automations.tsx +++ b/client/src/pages/Automations.tsx @@ -38,6 +38,7 @@ const TRIGGER_LABELS: Record = { '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 = {}; - 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() { При создании задачи При изменении статуса Сканирование QR + По расписанию (ежедневно, МСК) - {formTrigger !== 'manual' && ( + {formTrigger === 'schedule' && ( +
+ + setFormTriggerTime(e.target.value)} + /> +
+ )} + + {formTrigger !== 'manual' && formTrigger !== 'schedule' && (