@goodandready/dsh-cron 0.1.13 → 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 +303 -10
- package/lib/index.js +79 -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] {
|
|
@@ -123,6 +125,27 @@ const React = window.React || require('react');
|
|
|
123
125
|
.dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
|
124
126
|
.dsh-cron-card-title { font-size: 15px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
|
|
125
127
|
.dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); margin-bottom: 16px; line-height: 1.4; }
|
|
128
|
+
|
|
129
|
+
.dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid #2e2e2e; margin-bottom: 18px; padding-bottom: 2px; }
|
|
130
|
+
.dsh-cron-modal-tab { appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; padding: 8px 14px; font-size: 13.5px; color: #888; cursor: pointer; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
|
|
131
|
+
.dsh-cron-modal-tab:hover { color: #ccc; }
|
|
132
|
+
.dsh-cron-modal-tab[data-active="true"] { color: #fff; border-bottom-color: #ededed; }
|
|
133
|
+
.dsh-cron-history-list { display: flex; flex-direction: column; gap: 10px; max-height: 420px; overflow-y: auto; padding-right: 4px; }
|
|
134
|
+
.dsh-cron-history-item { background: #161616; border: 1px solid #2a2a2a; border-radius: 8px; padding: 12px 14px; font-size: 12.5px; }
|
|
135
|
+
.dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
|
136
|
+
.dsh-cron-history-status { font-weight: 600; font-size: 11.5px; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; }
|
|
137
|
+
.dsh-cron-history-status[data-status="success"] { background: rgba(16, 185, 129, 0.15); color: #10b981; }
|
|
138
|
+
.dsh-cron-history-status[data-status="error"] { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
|
|
139
|
+
.dsh-cron-history-time { color: #777; font-size: 12px; }
|
|
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; }
|
|
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; }
|
|
126
149
|
`;
|
|
127
150
|
|
|
128
151
|
function ensureStyles() {
|
|
@@ -220,6 +243,20 @@ const React = window.React || require('react');
|
|
|
220
243
|
const [formModel, setFormModel] = React.useState('');
|
|
221
244
|
const [formDelivery, setFormDelivery] = React.useState('isolated');
|
|
222
245
|
const [selectedTaskMeta, setSelectedTaskMeta] = React.useState(null);
|
|
246
|
+
const [modalTab, setModalTab] = React.useState('params');
|
|
247
|
+
const [taskHistory, setTaskHistory] = React.useState([]);
|
|
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);
|
|
223
260
|
|
|
224
261
|
// DSH dialog state
|
|
225
262
|
const [dshModalOpen, setDshModalOpen] = React.useState(false);
|
|
@@ -234,6 +271,7 @@ const React = window.React || require('react');
|
|
|
234
271
|
React.useEffect(() => {
|
|
235
272
|
const onKey = (e) => {
|
|
236
273
|
if (e.key !== 'Escape') return;
|
|
274
|
+
if (settingsModalOpen) { setSettingsModalOpen(false); return; }
|
|
237
275
|
if (manualModalOpen) { setManualModalOpen(false); return; }
|
|
238
276
|
if (dshModalOpen) { setDshModalOpen(false); return; }
|
|
239
277
|
if (dropdownOpen) { setDropdownOpen(false); return; }
|
|
@@ -241,7 +279,7 @@ const React = window.React || require('react');
|
|
|
241
279
|
};
|
|
242
280
|
document.addEventListener('keydown', onKey);
|
|
243
281
|
return () => document.removeEventListener('keydown', onKey);
|
|
244
|
-
}, [manualModalOpen, dshModalOpen, dropdownOpen, onClose]);
|
|
282
|
+
}, [manualModalOpen, settingsModalOpen, dshModalOpen, dropdownOpen, onClose]);
|
|
245
283
|
|
|
246
284
|
const fetchTasks = async () => {
|
|
247
285
|
try {
|
|
@@ -293,12 +331,76 @@ const React = window.React || require('react');
|
|
|
293
331
|
} catch (err) {}
|
|
294
332
|
};
|
|
295
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
|
+
|
|
296
396
|
const openManualModal = (preset) => {
|
|
297
397
|
setDropdownOpen(false);
|
|
298
398
|
setFormId(null);
|
|
299
399
|
setSelectedTaskMeta(null);
|
|
300
400
|
setFormType('llm');
|
|
301
401
|
setFormDelivery('isolated');
|
|
402
|
+
setFormNotifyTelegram(false);
|
|
403
|
+
setFormOnlyOnFailure(false);
|
|
302
404
|
if (preset) {
|
|
303
405
|
setFormTitle(preset.title || '');
|
|
304
406
|
setFormSchedule(preset.schedule || '');
|
|
@@ -317,10 +419,26 @@ const React = window.React || require('react');
|
|
|
317
419
|
setManualModalOpen(true);
|
|
318
420
|
};
|
|
319
421
|
|
|
422
|
+
const loadTaskHistory = async (taskId) => {
|
|
423
|
+
setHistoryLoading(true);
|
|
424
|
+
try {
|
|
425
|
+
const res = await fetch('/dsh-cron/action/' + encodeURIComponent(taskId) + '/history');
|
|
426
|
+
const data = await res.json();
|
|
427
|
+
if (data.ok) {
|
|
428
|
+
setTaskHistory(data.history || []);
|
|
429
|
+
}
|
|
430
|
+
} catch (e) {
|
|
431
|
+
console.error('[dsh-cron] history load failed:', e);
|
|
432
|
+
} finally {
|
|
433
|
+
setHistoryLoading(false);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
320
437
|
const openEditModal = (task) => {
|
|
321
438
|
setDropdownOpen(false);
|
|
322
439
|
setFormId(task.id);
|
|
323
440
|
setSelectedTaskMeta(task);
|
|
441
|
+
setModalTab('params');
|
|
324
442
|
setFormTitle(task.title || '');
|
|
325
443
|
setFormSchedule(task.schedule || '');
|
|
326
444
|
setFormType(task.type || 'llm');
|
|
@@ -328,8 +446,11 @@ const React = window.React || require('react');
|
|
|
328
446
|
setFormProvider(task.provider || '');
|
|
329
447
|
setFormModel(task.model || '');
|
|
330
448
|
setFormDelivery(task.delivery || 'isolated');
|
|
449
|
+
setFormNotifyTelegram(Boolean(task.notifyTelegram));
|
|
450
|
+
setFormOnlyOnFailure(Boolean(task.onlyOnFailure));
|
|
331
451
|
if (task.provider) loadModels(task.provider);
|
|
332
452
|
else if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
453
|
+
loadTaskHistory(task.id);
|
|
333
454
|
setManualModalOpen(true);
|
|
334
455
|
};
|
|
335
456
|
|
|
@@ -422,14 +543,26 @@ const React = window.React || require('react');
|
|
|
422
543
|
React.createElement('div', { className: 'dsh-cron-title' }, 'Запланированные задачи'),
|
|
423
544
|
React.createElement('div', { className: 'dsh-cron-subtitle' }, 'Попросите DSH планировать задачи, ставить напоминания или отслеживать обновления')
|
|
424
545
|
),
|
|
425
|
-
React.createElement('div', { style: {
|
|
426
|
-
React.createElement('button', {
|
|
427
|
-
'
|
|
428
|
-
|
|
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 & Оповещения'
|
|
429
556
|
),
|
|
430
|
-
|
|
431
|
-
React.createElement('button', { className: 'dsh-cron-
|
|
432
|
-
|
|
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
|
+
)
|
|
433
566
|
)
|
|
434
567
|
)
|
|
435
568
|
),
|
|
@@ -547,7 +680,7 @@ const React = window.React || require('react');
|
|
|
547
680
|
style: { maxWidth: '640px' },
|
|
548
681
|
onClick: (e) => e.stopPropagation()
|
|
549
682
|
},
|
|
550
|
-
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|
|
683
|
+
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: formId ? '12px' : '18px' } },
|
|
551
684
|
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, formId ? 'Редактирование задачи' : 'Новая запланированная задача'),
|
|
552
685
|
selectedTaskMeta && React.createElement('span', {
|
|
553
686
|
style: {
|
|
@@ -559,6 +692,55 @@ const React = window.React || require('react');
|
|
|
559
692
|
}
|
|
560
693
|
}, selectedTaskMeta.status === 'active' ? '● Активна' : '○ На паузе')
|
|
561
694
|
),
|
|
695
|
+
|
|
696
|
+
formId && React.createElement('div', { className: 'dsh-cron-modal-nav' },
|
|
697
|
+
React.createElement('button', {
|
|
698
|
+
type: 'button',
|
|
699
|
+
className: 'dsh-cron-modal-tab',
|
|
700
|
+
'data-active': modalTab === 'params',
|
|
701
|
+
onClick: () => setModalTab('params')
|
|
702
|
+
}, 'Параметры'),
|
|
703
|
+
React.createElement('button', {
|
|
704
|
+
type: 'button',
|
|
705
|
+
className: 'dsh-cron-modal-tab',
|
|
706
|
+
'data-active': modalTab === 'history',
|
|
707
|
+
onClick: () => { setModalTab('history'); if (formId) loadTaskHistory(formId); }
|
|
708
|
+
}, 'История запусков (' + (taskHistory.length || 0) + ')')
|
|
709
|
+
),
|
|
710
|
+
|
|
711
|
+
modalTab === 'history' && formId ? React.createElement('div', null,
|
|
712
|
+
historyLoading ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'Загрузка истории...') :
|
|
713
|
+
taskHistory.length === 0 ? React.createElement('div', { className: 'dsh-cron-history-empty' }, 'История запусков пока пуста') :
|
|
714
|
+
React.createElement('div', { className: 'dsh-cron-history-list' },
|
|
715
|
+
taskHistory.map((run) => React.createElement('div', { key: run.id, className: 'dsh-cron-history-item' },
|
|
716
|
+
React.createElement('div', { className: 'dsh-cron-history-head' },
|
|
717
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
718
|
+
React.createElement('span', {
|
|
719
|
+
className: 'dsh-cron-history-status',
|
|
720
|
+
'data-status': run.status
|
|
721
|
+
}, run.status === 'success' ? 'Успешно' : 'Ошибка'),
|
|
722
|
+
React.createElement('span', { className: 'dsh-cron-history-time' }, run.at ? new Date(run.at).toLocaleString() : '')
|
|
723
|
+
),
|
|
724
|
+
React.createElement('span', { style: { color: '#888', fontSize: '11.5px' } }, (run.durationMs || 0) + ' ms')
|
|
725
|
+
),
|
|
726
|
+
run.output && React.createElement('div', { className: 'dsh-cron-history-output' }, run.output),
|
|
727
|
+
run.error && React.createElement('div', { className: 'dsh-cron-history-output', style: { color: '#ef4444', borderColor: '#552222' } }, run.error)
|
|
728
|
+
))
|
|
729
|
+
),
|
|
730
|
+
React.createElement('div', { className: 'dsh-cron-modal-foot', style: { marginTop: '18px' } },
|
|
731
|
+
React.createElement('button', {
|
|
732
|
+
type: 'button',
|
|
733
|
+
className: 'dsh-cron-btn-secondary',
|
|
734
|
+
onClick: () => { if (formId) loadTaskHistory(formId); }
|
|
735
|
+
}, 'Обновить историю'),
|
|
736
|
+
React.createElement('button', {
|
|
737
|
+
type: 'button',
|
|
738
|
+
className: 'dsh-cron-btn-primary',
|
|
739
|
+
onClick: () => setManualModalOpen(false)
|
|
740
|
+
}, 'Закрыть')
|
|
741
|
+
)
|
|
742
|
+
) :
|
|
743
|
+
|
|
562
744
|
React.createElement('form', { onSubmit: handleSaveManual },
|
|
563
745
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
564
746
|
React.createElement('label', null, 'Название задачи'),
|
|
@@ -623,6 +805,36 @@ const React = window.React || require('react');
|
|
|
623
805
|
onChange: (e) => setFormPrompt(e.target.value)
|
|
624
806
|
})
|
|
625
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
|
+
),
|
|
626
838
|
selectedTaskMeta && React.createElement('div', {
|
|
627
839
|
style: {
|
|
628
840
|
background: 'rgba(255,255,255,0.03)',
|
|
@@ -645,7 +857,10 @@ const React = window.React || require('react');
|
|
|
645
857
|
type: 'button',
|
|
646
858
|
className: 'dsh-cron-btn-secondary',
|
|
647
859
|
style: { padding: '4px 10px', fontSize: '12px' },
|
|
648
|
-
onClick: () => {
|
|
860
|
+
onClick: async () => {
|
|
861
|
+
await handleRunNow(selectedTaskMeta.id);
|
|
862
|
+
if (formId) loadTaskHistory(formId);
|
|
863
|
+
}
|
|
649
864
|
}, 'Запустить сейчас')
|
|
650
865
|
),
|
|
651
866
|
React.createElement('div', { className: 'dsh-cron-modal-foot' },
|
|
@@ -663,6 +878,84 @@ const React = window.React || require('react');
|
|
|
663
878
|
)
|
|
664
879
|
),
|
|
665
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
|
+
),
|
|
666
959
|
// 2. "Создать с DSH" modal
|
|
667
960
|
dshModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => !dshLoading && setDshModalOpen(false) },
|
|
668
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',
|
|
@@ -177,6 +241,14 @@ export function apply(ctx, config) {
|
|
|
177
241
|
return;
|
|
178
242
|
}
|
|
179
243
|
|
|
244
|
+
const historyMatch = pathname.match(/^\/dsh-cron\/action\/([^/]+)\/history$/);
|
|
245
|
+
if (req.method === 'GET' && historyMatch) {
|
|
246
|
+
const taskId = historyMatch[1];
|
|
247
|
+
const history = store.getHistory(taskId, 30);
|
|
248
|
+
sendJson(res, 200, { ok: true, history });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
180
252
|
sendJson(res, 404, { ok: false, error: 'Action not found' });
|
|
181
253
|
} catch (err) {
|
|
182
254
|
sendJson(res, 500, { ok: false, error: err.message });
|
|
@@ -196,7 +268,9 @@ export function apply(ctx, config) {
|
|
|
196
268
|
delivery: { type: 'string', description: 'Режим запуска: current или isolated' },
|
|
197
269
|
type: { type: 'string', description: 'Тип задачи: "llm" (агент с LLM) или "script" (прямой запуск shell без LLM)' },
|
|
198
270
|
provider: { type: 'string', description: 'ID провайдера LLM для выполнения задачи' },
|
|
199
|
-
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 только при ошибках' }
|
|
200
274
|
},
|
|
201
275
|
output: {
|
|
202
276
|
schema: {
|
|
@@ -211,7 +285,7 @@ export function apply(ctx, config) {
|
|
|
211
285
|
return [{ type: 'text', text: value.message }];
|
|
212
286
|
}
|
|
213
287
|
},
|
|
214
|
-
execute: async ({ title, schedule, prompt, delivery, provider, model, type }) => {
|
|
288
|
+
execute: async ({ title, schedule, prompt, delivery, provider, model, type, notifyTelegram, onlyOnFailure }) => {
|
|
215
289
|
try {
|
|
216
290
|
const parsed = parseScheduleExpression(schedule);
|
|
217
291
|
const task = store.set({
|
|
@@ -223,6 +297,8 @@ export function apply(ctx, config) {
|
|
|
223
297
|
delivery: delivery || 'isolated',
|
|
224
298
|
provider: provider || undefined,
|
|
225
299
|
model: model || undefined,
|
|
300
|
+
notifyTelegram: Boolean(notifyTelegram),
|
|
301
|
+
onlyOnFailure: Boolean(onlyOnFailure),
|
|
226
302
|
status: 'active'
|
|
227
303
|
});
|
|
228
304
|
scheduler.scheduleTask(task);
|
|
@@ -370,4 +446,4 @@ export function apply(ctx, config) {
|
|
|
370
446
|
return;
|
|
371
447
|
}
|
|
372
448
|
});
|
|
373
|
-
}
|
|
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
|
+
}
|