@goodandready/dsh-cron 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/lib/client.js +215 -8
- package/lib/index.js +71 -3
- package/lib/scheduler.js +22 -3
- package/lib/store.js +32 -1
- package/lib/telegram.js +147 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,6 +108,14 @@ When autonomous agents need to set up delayed or recurring actions, they can inv
|
|
|
108
108
|
* **`cron_list_tasks`**: Retrieves an overview of active schedules and next scheduled run timestamps.
|
|
109
109
|
* **`cron_toggle_task`**: Enables or disables an existing task by `id`.
|
|
110
110
|
|
|
111
|
+
### 6. Telegram Notifications & Delivery Routing
|
|
112
|
+
Direct integration with the Telegram Bot API delivers execution reports and error traces straight to your messenger:
|
|
113
|
+
|
|
114
|
+
* **Auto-Detected or Custom Credentials**: Enter a custom `botToken` and `chatId` in the UI settings dialog, or automatically inherit default credentials from `dsh-messenger-gateway` in `settings.yaml`.
|
|
115
|
+
* **'Only on Failure' Mode (Issue #24)**: Prevent notification spam by enabling `onlyOnFailure` globally or on individual tasks. Clean runs remain silent, while non-zero exit codes or agent exceptions immediately dispatch an alert with stdout/stderr traces.
|
|
116
|
+
* **Markdown Formatting**: Messages are formatted with status badges (✅ / ❌), execution duration in milliseconds, schedule descriptions, and monospace code blocks.
|
|
117
|
+
* **Test Dispatch Button**: Verify Telegram connectivity on the spot before scheduling critical production jobs.
|
|
118
|
+
|
|
111
119
|
### 5. Schedule Expression Syntax
|
|
112
120
|
Powered by `croner`, supporting both standard 5-part/6-part cron expressions and user-friendly interval aliases:
|
|
113
121
|
|
package/lib/client.js
CHANGED
|
@@ -27,6 +27,8 @@ const React = window.React || require('react');
|
|
|
27
27
|
const ICON_ACTIVITY = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
|
|
28
28
|
const ICON_EDIT = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
|
|
29
29
|
const ICON_SEND = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
|
|
30
|
+
const ICON_SETTINGS = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>';
|
|
31
|
+
const ICON_TELEGRAM = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8-1.7 8.01c-.13.58-.47.72-.95.45l-2.6-1.92-1.25 1.21c-.14.14-.26.26-.53.26l.19-2.64 4.81-4.35c.21-.19-.05-.29-.32-.11L8.34 13.5 5.78 12.7c-.56-.17-.57-.56.12-.83l10-3.86c.46-.17.87.11.74.79z"/></svg>';
|
|
30
32
|
|
|
31
33
|
const STYLES = `
|
|
32
34
|
[data-pane=conversation], [class*=centerCol] {
|
|
@@ -137,6 +139,13 @@ const React = window.React || require('react');
|
|
|
137
139
|
.dsh-cron-history-time { color: #777; font-size: 12px; }
|
|
138
140
|
.dsh-cron-history-output { background: #0c0c0c; border: 1px solid #222; border-radius: 6px; padding: 8px 10px; font-family: monospace; font-size: 12px; color: #bbb; max-height: 140px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; margin-top: 6px; }
|
|
139
141
|
.dsh-cron-history-empty { text-align: center; padding: 36px 0; color: #666; font-size: 13.5px; }
|
|
142
|
+
.dsh-cron-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #ccc; cursor: pointer; user-select: none; }
|
|
143
|
+
.dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: #ededed; }
|
|
144
|
+
.dsh-cron-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
|
145
|
+
.dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
|
|
146
|
+
.dsh-cron-status-banner[data-type="success"] { background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.3); color: #10b981; }
|
|
147
|
+
.dsh-cron-status-banner[data-type="error"] { background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #ef4444; }
|
|
148
|
+
.dsh-cron-status-banner[data-type="info"] { background: rgba(59,130,246,0.15); border: 1px solid rgba(59,130,246,0.3); color: #60a5fa; }
|
|
140
149
|
`;
|
|
141
150
|
|
|
142
151
|
function ensureStyles() {
|
|
@@ -237,6 +246,17 @@ const React = window.React || require('react');
|
|
|
237
246
|
const [modalTab, setModalTab] = React.useState('params');
|
|
238
247
|
const [taskHistory, setTaskHistory] = React.useState([]);
|
|
239
248
|
const [historyLoading, setHistoryLoading] = React.useState(false);
|
|
249
|
+
const [formNotifyTelegram, setFormNotifyTelegram] = React.useState(false);
|
|
250
|
+
const [formOnlyOnFailure, setFormOnlyOnFailure] = React.useState(false);
|
|
251
|
+
|
|
252
|
+
// Settings modal state
|
|
253
|
+
const [settingsModalOpen, setSettingsModalOpen] = React.useState(false);
|
|
254
|
+
const [settingsBotToken, setSettingsBotToken] = React.useState('');
|
|
255
|
+
const [settingsChatId, setSettingsChatId] = React.useState('');
|
|
256
|
+
const [settingsNotifyGlobal, setSettingsNotifyGlobal] = React.useState(false);
|
|
257
|
+
const [settingsOnlyOnFailureGlobal, setSettingsOnlyOnFailureGlobal] = React.useState(false);
|
|
258
|
+
const [settingsHasDefault, setSettingsHasDefault] = React.useState(false);
|
|
259
|
+
const [testStatus, setTestStatus] = React.useState(null);
|
|
240
260
|
|
|
241
261
|
// DSH dialog state
|
|
242
262
|
const [dshModalOpen, setDshModalOpen] = React.useState(false);
|
|
@@ -251,6 +271,7 @@ const React = window.React || require('react');
|
|
|
251
271
|
React.useEffect(() => {
|
|
252
272
|
const onKey = (e) => {
|
|
253
273
|
if (e.key !== 'Escape') return;
|
|
274
|
+
if (settingsModalOpen) { setSettingsModalOpen(false); return; }
|
|
254
275
|
if (manualModalOpen) { setManualModalOpen(false); return; }
|
|
255
276
|
if (dshModalOpen) { setDshModalOpen(false); return; }
|
|
256
277
|
if (dropdownOpen) { setDropdownOpen(false); return; }
|
|
@@ -258,7 +279,7 @@ const React = window.React || require('react');
|
|
|
258
279
|
};
|
|
259
280
|
document.addEventListener('keydown', onKey);
|
|
260
281
|
return () => document.removeEventListener('keydown', onKey);
|
|
261
|
-
}, [manualModalOpen, dshModalOpen, dropdownOpen, onClose]);
|
|
282
|
+
}, [manualModalOpen, settingsModalOpen, dshModalOpen, dropdownOpen, onClose]);
|
|
262
283
|
|
|
263
284
|
const fetchTasks = async () => {
|
|
264
285
|
try {
|
|
@@ -310,12 +331,76 @@ const React = window.React || require('react');
|
|
|
310
331
|
} catch (err) {}
|
|
311
332
|
};
|
|
312
333
|
|
|
334
|
+
const openSettingsModal = async () => {
|
|
335
|
+
setTestStatus(null);
|
|
336
|
+
setSettingsModalOpen(true);
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetch('/dsh-cron/settings');
|
|
339
|
+
const data = await res.json();
|
|
340
|
+
if (data && data.ok && data.settings) {
|
|
341
|
+
setSettingsBotToken(data.settings.botToken || '');
|
|
342
|
+
setSettingsChatId(data.settings.chatId || '');
|
|
343
|
+
setSettingsNotifyGlobal(Boolean(data.settings.notifyTelegram));
|
|
344
|
+
setSettingsOnlyOnFailureGlobal(Boolean(data.settings.onlyOnFailure));
|
|
345
|
+
setSettingsHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
346
|
+
}
|
|
347
|
+
} catch (e) {}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const handleSaveSettings = async (e) => {
|
|
351
|
+
e.preventDefault();
|
|
352
|
+
try {
|
|
353
|
+
const res = await fetch('/dsh-cron/settings', {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
botToken: settingsBotToken,
|
|
358
|
+
chatId: settingsChatId,
|
|
359
|
+
notifyTelegram: settingsNotifyGlobal,
|
|
360
|
+
onlyOnFailure: settingsOnlyOnFailureGlobal
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
const data = await res.json();
|
|
364
|
+
if (data && data.ok) {
|
|
365
|
+
setSettingsModalOpen(false);
|
|
366
|
+
} else {
|
|
367
|
+
alert('Ошибка сохранения: ' + (data?.error || 'неизвестная ошибка'));
|
|
368
|
+
}
|
|
369
|
+
} catch (err) {
|
|
370
|
+
alert('Ошибка сохранения: ' + err.message);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const handleTestTelegram = async () => {
|
|
375
|
+
setTestStatus({ loading: true, message: 'Отправка тестового сообщения...' });
|
|
376
|
+
try {
|
|
377
|
+
const res = await fetch('/dsh-cron/telegram/test', {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
headers: { 'Content-Type': 'application/json' },
|
|
380
|
+
body: JSON.stringify({
|
|
381
|
+
botToken: settingsBotToken,
|
|
382
|
+
chatId: settingsChatId
|
|
383
|
+
})
|
|
384
|
+
});
|
|
385
|
+
const data = await res.json();
|
|
386
|
+
if (data && data.ok) {
|
|
387
|
+
setTestStatus({ loading: false, success: true, message: '✅ Тестовое сообщение успешно отправлено в Telegram!' });
|
|
388
|
+
} else {
|
|
389
|
+
setTestStatus({ loading: false, success: false, message: '❌ ' + (data?.error || 'Не удалось отправить сообщение') });
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
setTestStatus({ loading: false, success: false, message: '❌ ' + err.message });
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
313
396
|
const openManualModal = (preset) => {
|
|
314
397
|
setDropdownOpen(false);
|
|
315
398
|
setFormId(null);
|
|
316
399
|
setSelectedTaskMeta(null);
|
|
317
400
|
setFormType('llm');
|
|
318
401
|
setFormDelivery('isolated');
|
|
402
|
+
setFormNotifyTelegram(false);
|
|
403
|
+
setFormOnlyOnFailure(false);
|
|
319
404
|
if (preset) {
|
|
320
405
|
setFormTitle(preset.title || '');
|
|
321
406
|
setFormSchedule(preset.schedule || '');
|
|
@@ -361,6 +446,8 @@ const React = window.React || require('react');
|
|
|
361
446
|
setFormProvider(task.provider || '');
|
|
362
447
|
setFormModel(task.model || '');
|
|
363
448
|
setFormDelivery(task.delivery || 'isolated');
|
|
449
|
+
setFormNotifyTelegram(Boolean(task.notifyTelegram));
|
|
450
|
+
setFormOnlyOnFailure(Boolean(task.onlyOnFailure));
|
|
364
451
|
if (task.provider) loadModels(task.provider);
|
|
365
452
|
else if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
366
453
|
loadTaskHistory(task.id);
|
|
@@ -456,14 +543,26 @@ const React = window.React || require('react');
|
|
|
456
543
|
React.createElement('div', { className: 'dsh-cron-title' }, 'Запланированные задачи'),
|
|
457
544
|
React.createElement('div', { className: 'dsh-cron-subtitle' }, 'Попросите DSH планировать задачи, ставить напоминания или отслеживать обновления')
|
|
458
545
|
),
|
|
459
|
-
React.createElement('div', { style: {
|
|
460
|
-
React.createElement('button', {
|
|
461
|
-
'
|
|
462
|
-
|
|
546
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
|
|
547
|
+
React.createElement('button', {
|
|
548
|
+
type: 'button',
|
|
549
|
+
className: 'dsh-cron-btn-secondary',
|
|
550
|
+
style: { display: 'flex', alignItems: 'center', gap: '6px', height: '36px', padding: '0 14px' },
|
|
551
|
+
title: 'Настройки Telegram и уведомлений',
|
|
552
|
+
onClick: openSettingsModal
|
|
553
|
+
},
|
|
554
|
+
React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
|
|
555
|
+
'Telegram & Оповещения'
|
|
463
556
|
),
|
|
464
|
-
|
|
465
|
-
React.createElement('button', { className: 'dsh-cron-
|
|
466
|
-
|
|
557
|
+
React.createElement('div', { style: { position: 'relative' } },
|
|
558
|
+
React.createElement('button', { className: 'dsh-cron-create-btn', onClick: () => setDropdownOpen(!dropdownOpen) },
|
|
559
|
+
'Создать',
|
|
560
|
+
React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } })
|
|
561
|
+
),
|
|
562
|
+
dropdownOpen && React.createElement('div', { className: 'dsh-cron-dropdown' },
|
|
563
|
+
React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: openDshModal }, '💬 Создать с DSH'),
|
|
564
|
+
React.createElement('button', { className: 'dsh-cron-dropdown-item', onClick: () => openManualModal(null) }, '✏️ Настроить вручную')
|
|
565
|
+
)
|
|
467
566
|
)
|
|
468
567
|
)
|
|
469
568
|
),
|
|
@@ -706,6 +805,36 @@ const React = window.React || require('react');
|
|
|
706
805
|
onChange: (e) => setFormPrompt(e.target.value)
|
|
707
806
|
})
|
|
708
807
|
),
|
|
808
|
+
React.createElement('div', {
|
|
809
|
+
style: {
|
|
810
|
+
background: 'rgba(255,255,255,0.02)',
|
|
811
|
+
border: '1px solid #2e2e2e',
|
|
812
|
+
borderRadius: '8px',
|
|
813
|
+
padding: '12px 14px',
|
|
814
|
+
marginBottom: '16px'
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
React.createElement('div', { style: { fontWeight: 500, fontSize: '13px', color: '#eee', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
818
|
+
React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
|
|
819
|
+
'Уведомления в Telegram'
|
|
820
|
+
),
|
|
821
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: formNotifyTelegram ? '8px' : '0' } },
|
|
822
|
+
React.createElement('input', {
|
|
823
|
+
type: 'checkbox',
|
|
824
|
+
checked: formNotifyTelegram,
|
|
825
|
+
onChange: (e) => setFormNotifyTelegram(e.target.checked)
|
|
826
|
+
}),
|
|
827
|
+
React.createElement('span', null, 'Отправлять отчет о выполнении в Telegram')
|
|
828
|
+
),
|
|
829
|
+
formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
|
|
830
|
+
React.createElement('input', {
|
|
831
|
+
type: 'checkbox',
|
|
832
|
+
checked: formOnlyOnFailure,
|
|
833
|
+
onChange: (e) => setFormOnlyOnFailure(e.target.checked)
|
|
834
|
+
}),
|
|
835
|
+
React.createElement('span', null, 'Только при ошибках (onlyOnFailure / silent mode при успехе)')
|
|
836
|
+
)
|
|
837
|
+
),
|
|
709
838
|
selectedTaskMeta && React.createElement('div', {
|
|
710
839
|
style: {
|
|
711
840
|
background: 'rgba(255,255,255,0.03)',
|
|
@@ -749,6 +878,84 @@ const React = window.React || require('react');
|
|
|
749
878
|
)
|
|
750
879
|
),
|
|
751
880
|
|
|
881
|
+
// 1.5 Settings Modal
|
|
882
|
+
settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
|
|
883
|
+
React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
|
|
884
|
+
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|
|
885
|
+
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0, display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
886
|
+
React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: '#60a5fa' }, dangerouslySetInnerHTML: { __html: ICON_TELEGRAM } }),
|
|
887
|
+
'Настройки Telegram и уведомлений'
|
|
888
|
+
),
|
|
889
|
+
React.createElement('button', {
|
|
890
|
+
type: 'button',
|
|
891
|
+
style: { background: 'none', border: 'none', color: '#888', cursor: 'pointer', fontSize: '18px' },
|
|
892
|
+
onClick: () => setSettingsModalOpen(false)
|
|
893
|
+
}, '✕')
|
|
894
|
+
),
|
|
895
|
+
React.createElement('form', { onSubmit: handleSaveSettings },
|
|
896
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
897
|
+
React.createElement('label', null, 'Telegram Bot Token'),
|
|
898
|
+
React.createElement('input', {
|
|
899
|
+
type: 'password',
|
|
900
|
+
value: settingsBotToken,
|
|
901
|
+
placeholder: settingsHasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
|
|
902
|
+
onChange: (e) => setSettingsBotToken(e.target.value)
|
|
903
|
+
}),
|
|
904
|
+
settingsHasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Обнаружен настроенный бот в профиле DSH')
|
|
905
|
+
),
|
|
906
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
907
|
+
React.createElement('label', null, 'Telegram Chat ID'),
|
|
908
|
+
React.createElement('input', {
|
|
909
|
+
value: settingsChatId,
|
|
910
|
+
placeholder: 'Например: 345678901',
|
|
911
|
+
onChange: (e) => setSettingsChatId(e.target.value)
|
|
912
|
+
})
|
|
913
|
+
),
|
|
914
|
+
React.createElement('div', { style: { background: 'rgba(255,255,255,0.02)', border: '1px solid #2e2e2e', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px' } },
|
|
915
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginBottom: '8px' } },
|
|
916
|
+
React.createElement('input', {
|
|
917
|
+
type: 'checkbox',
|
|
918
|
+
checked: settingsNotifyGlobal,
|
|
919
|
+
onChange: (e) => setSettingsNotifyGlobal(e.target.checked)
|
|
920
|
+
}),
|
|
921
|
+
React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
|
|
922
|
+
),
|
|
923
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
924
|
+
React.createElement('input', {
|
|
925
|
+
type: 'checkbox',
|
|
926
|
+
checked: settingsOnlyOnFailureGlobal,
|
|
927
|
+
onChange: (e) => setSettingsOnlyOnFailureGlobal(e.target.checked)
|
|
928
|
+
}),
|
|
929
|
+
React.createElement('span', null, 'Глобальный режим: Только при ошибках (silent при успехе)')
|
|
930
|
+
)
|
|
931
|
+
),
|
|
932
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' } },
|
|
933
|
+
React.createElement('button', {
|
|
934
|
+
type: 'button',
|
|
935
|
+
className: 'dsh-cron-btn-secondary',
|
|
936
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: '6px' },
|
|
937
|
+
disabled: testStatus?.loading,
|
|
938
|
+
onClick: handleTestTelegram
|
|
939
|
+
}, testStatus?.loading ? 'Отправка...' : '🔔 Проверить отправку в Telegram')
|
|
940
|
+
),
|
|
941
|
+
testStatus && testStatus.message && React.createElement('div', {
|
|
942
|
+
className: 'dsh-cron-status-banner',
|
|
943
|
+
'data-type': testStatus.success ? 'success' : 'error'
|
|
944
|
+
}, testStatus.message),
|
|
945
|
+
React.createElement('div', { className: 'dsh-cron-modal-foot' },
|
|
946
|
+
React.createElement('button', {
|
|
947
|
+
type: 'button',
|
|
948
|
+
className: 'dsh-cron-btn-secondary',
|
|
949
|
+
onClick: () => setSettingsModalOpen(false)
|
|
950
|
+
}, 'Отмена'),
|
|
951
|
+
React.createElement('button', {
|
|
952
|
+
type: 'submit',
|
|
953
|
+
className: 'dsh-cron-btn-primary'
|
|
954
|
+
}, 'Сохранить настройки')
|
|
955
|
+
)
|
|
956
|
+
)
|
|
957
|
+
)
|
|
958
|
+
),
|
|
752
959
|
// 2. "Создать с DSH" modal
|
|
753
960
|
dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
|
|
754
961
|
React.createElement('div', { className: 'dsh-cron-modal', onClick: (e) => e.stopPropagation() },
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { TaskScheduler, parseScheduleExpression } from './scheduler.js';
|
|
|
3
3
|
import { SessionRunner } from './runner.js';
|
|
4
4
|
import { getModelsHandler } from './models-handler.js';
|
|
5
5
|
import { chatStartHandler } from './chat-start.js';
|
|
6
|
+
import { sendTelegramMessage } from './telegram.js';
|
|
6
7
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
7
8
|
import { randomUUID } from 'node:crypto';
|
|
8
9
|
|
|
@@ -106,6 +107,8 @@ export function apply(ctx, config) {
|
|
|
106
107
|
status: body.status || 'active',
|
|
107
108
|
provider: body.provider || undefined,
|
|
108
109
|
model: body.model || undefined,
|
|
110
|
+
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : false,
|
|
111
|
+
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : false,
|
|
109
112
|
});
|
|
110
113
|
scheduler.scheduleTask(task);
|
|
111
114
|
sendJson(res, 200, { ok: true, task });
|
|
@@ -137,6 +140,67 @@ export function apply(ctx, config) {
|
|
|
137
140
|
}
|
|
138
141
|
}), 'dsh-cron: /chat/start');
|
|
139
142
|
|
|
143
|
+
// GET & POST /dsh-cron/settings
|
|
144
|
+
ctx.effect(() => ctx.webServer.register({
|
|
145
|
+
kind: 'exact',
|
|
146
|
+
path: '/dsh-cron/settings',
|
|
147
|
+
handler: async (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
if (req.method === 'GET') {
|
|
150
|
+
sendJson(res, 200, { ok: true, settings: store.getSettings() });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (req.method === 'POST') {
|
|
154
|
+
const body = await parseJsonBody(req);
|
|
155
|
+
const updated = store.saveSettings(body);
|
|
156
|
+
sendJson(res, 200, { ok: true, settings: updated });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}), 'dsh-cron: /settings');
|
|
165
|
+
|
|
166
|
+
// POST /dsh-cron/telegram/test
|
|
167
|
+
ctx.effect(() => ctx.webServer.register({
|
|
168
|
+
kind: 'exact',
|
|
169
|
+
path: '/dsh-cron/telegram/test',
|
|
170
|
+
handler: async (req, res) => {
|
|
171
|
+
try {
|
|
172
|
+
if (req.method !== 'POST') {
|
|
173
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const body = await parseJsonBody(req);
|
|
177
|
+
const currentSettings = store.getSettings();
|
|
178
|
+
const botToken = (body.botToken || currentSettings.botToken || '').trim();
|
|
179
|
+
const chatId = (body.chatId || currentSettings.chatId || '').trim();
|
|
180
|
+
|
|
181
|
+
if (!botToken) {
|
|
182
|
+
sendJson(res, 400, { ok: false, error: 'Укажите Telegram Bot Token' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!chatId) {
|
|
186
|
+
sendJson(res, 400, { ok: false, error: 'Укажите Telegram Chat ID' });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await sendTelegramMessage({
|
|
191
|
+
botToken,
|
|
192
|
+
chatId,
|
|
193
|
+
text: '⏰ *DSH Cron — Проверка связи*\nТестовое уведомление успешно доставлено! Планировщик готов к отправке отчётов.',
|
|
194
|
+
parseMode: 'Markdown'
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
sendJson(res, 200, { ok: true, message: 'Сообщение успешно отправлено в Telegram' });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}), 'dsh-cron: /telegram/test');
|
|
203
|
+
|
|
140
204
|
// Prefix routes for actions (/dsh-cron/action/*)
|
|
141
205
|
ctx.effect(() => ctx.webServer.register({
|
|
142
206
|
kind: 'prefix',
|
|
@@ -204,7 +268,9 @@ export function apply(ctx, config) {
|
|
|
204
268
|
delivery: { type: 'string', description: 'Режим запуска: current или isolated' },
|
|
205
269
|
type: { type: 'string', description: 'Тип задачи: "llm" (агент с LLM) или "script" (прямой запуск shell без LLM)' },
|
|
206
270
|
provider: { type: 'string', description: 'ID провайдера LLM для выполнения задачи' },
|
|
207
|
-
model: { type: 'string', description: 'ID модели LLM для выполнения задачи' }
|
|
271
|
+
model: { type: 'string', description: 'ID модели LLM для выполнения задачи' },
|
|
272
|
+
notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
|
|
273
|
+
onlyOnFailure: { type: 'boolean', description: 'Отправлять в Telegram только при ошибках' }
|
|
208
274
|
},
|
|
209
275
|
output: {
|
|
210
276
|
schema: {
|
|
@@ -219,7 +285,7 @@ export function apply(ctx, config) {
|
|
|
219
285
|
return [{ type: 'text', text: value.message }];
|
|
220
286
|
}
|
|
221
287
|
},
|
|
222
|
-
execute: async ({ title, schedule, prompt, delivery, provider, model, type }) => {
|
|
288
|
+
execute: async ({ title, schedule, prompt, delivery, provider, model, type, notifyTelegram, onlyOnFailure }) => {
|
|
223
289
|
try {
|
|
224
290
|
const parsed = parseScheduleExpression(schedule);
|
|
225
291
|
const task = store.set({
|
|
@@ -231,6 +297,8 @@ export function apply(ctx, config) {
|
|
|
231
297
|
delivery: delivery || 'isolated',
|
|
232
298
|
provider: provider || undefined,
|
|
233
299
|
model: model || undefined,
|
|
300
|
+
notifyTelegram: Boolean(notifyTelegram),
|
|
301
|
+
onlyOnFailure: Boolean(onlyOnFailure),
|
|
234
302
|
status: 'active'
|
|
235
303
|
});
|
|
236
304
|
scheduler.scheduleTask(task);
|
|
@@ -378,4 +446,4 @@ export function apply(ctx, config) {
|
|
|
378
446
|
return;
|
|
379
447
|
}
|
|
380
448
|
});
|
|
381
|
-
}
|
|
449
|
+
}
|
package/lib/scheduler.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Cron } from 'croner';
|
|
2
|
+
import { shouldNotifyTask, formatTaskTelegramMessage, sendTelegramMessage } from './telegram.js';
|
|
2
3
|
|
|
3
4
|
function pad(v) {
|
|
4
5
|
return String(v).padStart(2, '0');
|
|
@@ -145,13 +146,31 @@ export class TaskScheduler {
|
|
|
145
146
|
error = err.message || String(err);
|
|
146
147
|
} finally {
|
|
147
148
|
const durationMs = Date.now() - start;
|
|
148
|
-
|
|
149
|
+
const runInfo = {
|
|
149
150
|
at: start,
|
|
150
151
|
status,
|
|
151
152
|
durationMs,
|
|
152
153
|
output,
|
|
153
154
|
error,
|
|
154
|
-
}
|
|
155
|
+
};
|
|
156
|
+
this.store.recordRun(taskId, runInfo);
|
|
157
|
+
|
|
158
|
+
// Telegram notification delivery
|
|
159
|
+
try {
|
|
160
|
+
const settings = typeof this.store.getSettings === 'function' ? this.store.getSettings() : {};
|
|
161
|
+
if (shouldNotifyTask(task, runInfo, settings)) {
|
|
162
|
+
const botToken = settings.botToken;
|
|
163
|
+
const chatId = settings.chatId;
|
|
164
|
+
if (botToken && chatId) {
|
|
165
|
+
const text = formatTaskTelegramMessage(task, runInfo);
|
|
166
|
+
sendTelegramMessage({ botToken, chatId, text }).catch((err) => {
|
|
167
|
+
console.error(`[dsh-cron] telegram delivery failed for task "${task.title}":`, err.message);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (notifyErr) {
|
|
172
|
+
console.error('[dsh-cron] notification error:', notifyErr.message);
|
|
173
|
+
}
|
|
155
174
|
|
|
156
175
|
const job = this.jobs.get(taskId);
|
|
157
176
|
if (job) {
|
|
@@ -202,4 +221,4 @@ export class TaskScheduler {
|
|
|
202
221
|
return null;
|
|
203
222
|
}
|
|
204
223
|
}
|
|
205
|
-
}
|
|
224
|
+
}
|
package/lib/store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { getDshDefaultTelegramCredentials } from './telegram.js';
|
|
3
4
|
|
|
4
5
|
export function getDefaultStorePath() {
|
|
5
6
|
const base = process.env.DSH_DATA_DIR || path.join(process.env.HOME || '.', '.dsh', 'data');
|
|
@@ -11,6 +12,7 @@ export class TaskStore {
|
|
|
11
12
|
this.filePath = filePath || getDefaultStorePath();
|
|
12
13
|
this.tasks = new Map();
|
|
13
14
|
this.history = new Map();
|
|
15
|
+
this.settings = {};
|
|
14
16
|
this.init();
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -33,6 +35,9 @@ export class TaskStore {
|
|
|
33
35
|
this.history.set(k, Array.isArray(v) ? v : []);
|
|
34
36
|
}
|
|
35
37
|
}
|
|
38
|
+
if (data.settings && typeof data.settings === 'object') {
|
|
39
|
+
this.settings = data.settings;
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
} catch (err) {
|
|
38
43
|
console.error('[dsh-cron] store read error:', err.message);
|
|
@@ -50,6 +55,7 @@ export class TaskStore {
|
|
|
50
55
|
updatedAt: new Date().toISOString(),
|
|
51
56
|
tasks: Array.from(this.tasks.values()),
|
|
52
57
|
history: Object.fromEntries(this.history.entries()),
|
|
58
|
+
settings: this.settings || {},
|
|
53
59
|
};
|
|
54
60
|
const tmp = this.filePath + '.tmp.' + Date.now();
|
|
55
61
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
@@ -59,6 +65,29 @@ export class TaskStore {
|
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
getSettings() {
|
|
69
|
+
const defaults = getDshDefaultTelegramCredentials();
|
|
70
|
+
return {
|
|
71
|
+
botToken: this.settings.botToken !== undefined ? this.settings.botToken : (defaults.botToken || ''),
|
|
72
|
+
chatId: this.settings.chatId !== undefined ? this.settings.chatId : (defaults.chatId || ''),
|
|
73
|
+
notifyTelegram: Boolean(this.settings.notifyTelegram),
|
|
74
|
+
onlyOnFailure: Boolean(this.settings.onlyOnFailure),
|
|
75
|
+
hasDefaultCredentials: Boolean(defaults.botToken && defaults.chatId),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
saveSettings(newSettings = {}) {
|
|
80
|
+
this.settings = {
|
|
81
|
+
...this.settings,
|
|
82
|
+
botToken: newSettings.botToken !== undefined ? String(newSettings.botToken).trim() : (this.settings.botToken || ''),
|
|
83
|
+
chatId: newSettings.chatId !== undefined ? String(newSettings.chatId).trim() : (this.settings.chatId || ''),
|
|
84
|
+
notifyTelegram: newSettings.notifyTelegram !== undefined ? Boolean(newSettings.notifyTelegram) : Boolean(this.settings.notifyTelegram),
|
|
85
|
+
onlyOnFailure: newSettings.onlyOnFailure !== undefined ? Boolean(newSettings.onlyOnFailure) : Boolean(this.settings.onlyOnFailure),
|
|
86
|
+
};
|
|
87
|
+
this.save();
|
|
88
|
+
return this.getSettings();
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
list(filter = {}) {
|
|
63
92
|
let all = Array.from(this.tasks.values());
|
|
64
93
|
if (filter.status && filter.status !== 'all') {
|
|
@@ -96,6 +125,8 @@ export class TaskStore {
|
|
|
96
125
|
delivery: task.delivery !== undefined ? task.delivery : (prev.delivery || 'isolated'),
|
|
97
126
|
provider: task.provider !== undefined ? task.provider : prev.provider,
|
|
98
127
|
model: task.model !== undefined ? task.model : prev.model,
|
|
128
|
+
notifyTelegram: task.notifyTelegram !== undefined ? Boolean(task.notifyTelegram) : (prev.notifyTelegram || false),
|
|
129
|
+
onlyOnFailure: task.onlyOnFailure !== undefined ? Boolean(task.onlyOnFailure) : (prev.onlyOnFailure || false),
|
|
99
130
|
createdAt: prev.createdAt || task.createdAt || now,
|
|
100
131
|
updatedAt: now,
|
|
101
132
|
lastRunAt: task.lastRunAt !== undefined ? task.lastRunAt : (prev.lastRunAt || null),
|
|
@@ -141,4 +172,4 @@ export class TaskStore {
|
|
|
141
172
|
const runs = this.history.get(id) || [];
|
|
142
173
|
return runs.slice(0, limit);
|
|
143
174
|
}
|
|
144
|
-
}
|
|
175
|
+
}
|
package/lib/telegram.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract simple key from YAML text without external dependencies.
|
|
7
|
+
*/
|
|
8
|
+
export function extractSettingFromYaml(text, section, key) {
|
|
9
|
+
if (!text) return undefined;
|
|
10
|
+
const lines = text.split('\n');
|
|
11
|
+
let inside = false;
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (/^[^\s#]/.test(line)) inside = line.startsWith(section + ':');
|
|
14
|
+
if (!inside) continue;
|
|
15
|
+
const hit = new RegExp('^[ \t]+' + key + ':[ \t]*(.+?)[ \t]*$').exec(line);
|
|
16
|
+
if (hit) return hit[1].replace(/^['"]|['"]$/g, '').trim();
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract first allowed chat id from YAML allowedUserIds list.
|
|
23
|
+
*/
|
|
24
|
+
export function extractFirstAllowedChatId(text) {
|
|
25
|
+
if (!text) return undefined;
|
|
26
|
+
const lines = text.split('\n');
|
|
27
|
+
let armed = false;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (/allowedUserIds:/.test(line)) { armed = true; continue; }
|
|
30
|
+
if (!armed) continue;
|
|
31
|
+
const hit = /^\s*-\s*(\d+)\s*$/.exec(line);
|
|
32
|
+
if (hit) return hit[1];
|
|
33
|
+
if (/^\s*[a-zA-Z]/.test(line)) return undefined;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read system DSH settings for fallback credentials.
|
|
40
|
+
*/
|
|
41
|
+
export function getDshDefaultTelegramCredentials() {
|
|
42
|
+
try {
|
|
43
|
+
const p = path.join(process.env.HOME || os.homedir(), '.dsh', 'settings.yaml');
|
|
44
|
+
if (fs.existsSync(p)) {
|
|
45
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
46
|
+
const botToken = extractSettingFromYaml(raw, 'dsh-messenger-gateway', 'botToken');
|
|
47
|
+
const chatId = extractFirstAllowedChatId(raw);
|
|
48
|
+
return { botToken, chatId };
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
return { botToken: undefined, chatId: undefined };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Escape markdown special characters for Telegram legacy markdown.
|
|
56
|
+
*/
|
|
57
|
+
export function escapeMarkdown(str) {
|
|
58
|
+
if (!str) return '';
|
|
59
|
+
return String(str).replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format task execution report for Telegram.
|
|
64
|
+
*/
|
|
65
|
+
export function formatTaskTelegramMessage(task, runInfo) {
|
|
66
|
+
const isSuccess = runInfo.status === 'success';
|
|
67
|
+
const statusEmoji = isSuccess ? '✅' : '❌';
|
|
68
|
+
const statusText = isSuccess ? 'Успешно' : 'Ошибка';
|
|
69
|
+
const duration = runInfo.durationMs != null ? `${runInfo.durationMs} ms` : '—';
|
|
70
|
+
const title = task.title || 'Задача';
|
|
71
|
+
const schedule = task.scheduleText || task.schedule || '';
|
|
72
|
+
|
|
73
|
+
const lines = [
|
|
74
|
+
`⏰ *DSH Cron:* ${title}`,
|
|
75
|
+
`*Статус:* ${statusEmoji} ${statusText}`,
|
|
76
|
+
schedule ? `*Расписание:* ${schedule}` : null,
|
|
77
|
+
`*Длительность:* ${duration}`,
|
|
78
|
+
].filter(Boolean);
|
|
79
|
+
|
|
80
|
+
if (runInfo.error) {
|
|
81
|
+
const errText = String(runInfo.error).slice(0, 1500);
|
|
82
|
+
lines.push('', '*Ошибка:*', '```', errText, '```');
|
|
83
|
+
} else if (runInfo.output && isSuccess) {
|
|
84
|
+
const outText = String(runInfo.output).trim().slice(0, 2000);
|
|
85
|
+
if (outText) {
|
|
86
|
+
lines.push('', '*Вывод:*', '```', outText, '```');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lines.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send telegram message via Bot API.
|
|
95
|
+
*/
|
|
96
|
+
export async function sendTelegramMessage({ botToken, chatId, text, parseMode = 'Markdown' }) {
|
|
97
|
+
if (!botToken) throw new Error('Telegram botToken не указан');
|
|
98
|
+
if (!chatId) throw new Error('Telegram chatId не указан');
|
|
99
|
+
|
|
100
|
+
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
|
101
|
+
const body = {
|
|
102
|
+
chat_id: chatId,
|
|
103
|
+
text,
|
|
104
|
+
disable_notification: false,
|
|
105
|
+
};
|
|
106
|
+
if (parseMode) body.parse_mode = parseMode;
|
|
107
|
+
|
|
108
|
+
const res = await fetch(url, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const data = await res.json().catch(() => ({}));
|
|
115
|
+
if (!res.ok || !data.ok) {
|
|
116
|
+
if (parseMode && data.description && data.description.includes("can't parse entities")) {
|
|
117
|
+
delete body.parse_mode;
|
|
118
|
+
const retryRes = await fetch(url, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
});
|
|
123
|
+
const retryData = await retryRes.json().catch(() => ({}));
|
|
124
|
+
if (retryRes.ok && retryData.ok) {
|
|
125
|
+
return retryData;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const msg = data.description || res.statusText || `HTTP ${res.status}`;
|
|
129
|
+
throw new Error(`Telegram API Error: ${msg}`);
|
|
130
|
+
}
|
|
131
|
+
return data;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Determine if a notification should be sent for a given task and run result.
|
|
136
|
+
*/
|
|
137
|
+
export function shouldNotifyTask(task, runInfo, globalSettings = {}) {
|
|
138
|
+
const isEnabled = task.notifyTelegram ?? globalSettings.notifyTelegram ?? false;
|
|
139
|
+
if (!isEnabled) return false;
|
|
140
|
+
|
|
141
|
+
const onlyOnFail = task.onlyOnFailure ?? globalSettings.onlyOnFailure ?? false;
|
|
142
|
+
if (onlyOnFail && runInfo.status !== 'error') {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
}
|