@goodandready/dsh-cron 0.1.9 → 0.1.11
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/lib/client.js +143 -30
- package/lib/index.js +3 -1
- package/lib/prompt.js +41 -8
- package/lib/runner.js +23 -13
- package/lib/store.js +17 -12
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -25,6 +25,7 @@ const React = window.React || require('react');
|
|
|
25
25
|
const ICON_BELL = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>';
|
|
26
26
|
const ICON_CLIPBOARD = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>';
|
|
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
|
+
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>';
|
|
28
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>';
|
|
29
30
|
|
|
30
31
|
const STYLES = `
|
|
@@ -73,7 +74,10 @@ const React = window.React || require('react');
|
|
|
73
74
|
.dsh-cron-tab { appearance: none; background: transparent; border: none; padding: 6px 14px; border-radius: 9999px; color: #9ca3af; font-size: 13.5px; cursor: pointer; }
|
|
74
75
|
.dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: #fff; font-weight: 500; }
|
|
75
76
|
.dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; }
|
|
76
|
-
.dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 12px; }
|
|
77
|
+
.dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 12px; cursor: pointer; transition: background 0.15s, border-color 0.15s; }
|
|
78
|
+
.dsh-cron-task-item:hover { background: var(--dsw-alias-bg-layer-4, #242424); border-color: #444; }
|
|
79
|
+
.dsh-cron-task-prompt-preview { font-size: 12.5px; color: #888; margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; line-height: 1.3; }
|
|
80
|
+
.dsh-cron-type-tag { display: inline-block; font-size: 10.5px; padding: 1px 6px; border-radius: 4px; text-transform: uppercase; font-weight: 600; margin-left: 8px; vertical-align: middle; }
|
|
77
81
|
.dsh-cron-task-item:hover { border-color: #444; }
|
|
78
82
|
.dsh-cron-task-status-btn { background: none; border: 1px solid #444; border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; color: #888; cursor: pointer; flex-shrink: 0; }
|
|
79
83
|
.dsh-cron-task-status-btn[data-active="true"] { color: #10b981; border-color: #10b981; }
|
|
@@ -205,13 +209,17 @@ const React = window.React || require('react');
|
|
|
205
209
|
const [recs, setRecs] = React.useState([]);
|
|
206
210
|
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
|
207
211
|
|
|
208
|
-
// Manual modal state
|
|
212
|
+
// Manual / Edit modal state
|
|
209
213
|
const [manualModalOpen, setManualModalOpen] = React.useState(false);
|
|
214
|
+
const [formId, setFormId] = React.useState(null);
|
|
210
215
|
const [formTitle, setFormTitle] = React.useState('');
|
|
211
216
|
const [formSchedule, setFormSchedule] = React.useState('');
|
|
217
|
+
const [formType, setFormType] = React.useState('llm');
|
|
212
218
|
const [formPrompt, setFormPrompt] = React.useState('');
|
|
213
219
|
const [formProvider, setFormProvider] = React.useState('');
|
|
214
220
|
const [formModel, setFormModel] = React.useState('');
|
|
221
|
+
const [formDelivery, setFormDelivery] = React.useState('isolated');
|
|
222
|
+
const [selectedTaskMeta, setSelectedTaskMeta] = React.useState(null);
|
|
215
223
|
|
|
216
224
|
// DSH dialog state
|
|
217
225
|
const [dshModalOpen, setDshModalOpen] = React.useState(false);
|
|
@@ -287,6 +295,10 @@ const React = window.React || require('react');
|
|
|
287
295
|
|
|
288
296
|
const openManualModal = (preset) => {
|
|
289
297
|
setDropdownOpen(false);
|
|
298
|
+
setFormId(null);
|
|
299
|
+
setSelectedTaskMeta(null);
|
|
300
|
+
setFormType('llm');
|
|
301
|
+
setFormDelivery('isolated');
|
|
290
302
|
if (preset) {
|
|
291
303
|
setFormTitle(preset.title || '');
|
|
292
304
|
setFormSchedule(preset.schedule || '');
|
|
@@ -305,6 +317,22 @@ const React = window.React || require('react');
|
|
|
305
317
|
setManualModalOpen(true);
|
|
306
318
|
};
|
|
307
319
|
|
|
320
|
+
const openEditModal = (task) => {
|
|
321
|
+
setDropdownOpen(false);
|
|
322
|
+
setFormId(task.id);
|
|
323
|
+
setSelectedTaskMeta(task);
|
|
324
|
+
setFormTitle(task.title || '');
|
|
325
|
+
setFormSchedule(task.schedule || '');
|
|
326
|
+
setFormType(task.type || 'llm');
|
|
327
|
+
setFormPrompt(task.prompt || '');
|
|
328
|
+
setFormProvider(task.provider || '');
|
|
329
|
+
setFormModel(task.model || '');
|
|
330
|
+
setFormDelivery(task.delivery || 'isolated');
|
|
331
|
+
if (task.provider) loadModels(task.provider);
|
|
332
|
+
else if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
333
|
+
setManualModalOpen(true);
|
|
334
|
+
};
|
|
335
|
+
|
|
308
336
|
const handleProviderChange = (e) => {
|
|
309
337
|
const prov = e.target.value;
|
|
310
338
|
setFormProvider(prov);
|
|
@@ -315,19 +343,23 @@ const React = window.React || require('react');
|
|
|
315
343
|
const handleSaveManual = async (e) => {
|
|
316
344
|
e.preventDefault();
|
|
317
345
|
try {
|
|
346
|
+
const payload = {
|
|
347
|
+
id: formId || undefined,
|
|
348
|
+
title: formTitle,
|
|
349
|
+
schedule: formSchedule,
|
|
350
|
+
prompt: formPrompt,
|
|
351
|
+
type: formType,
|
|
352
|
+
delivery: formDelivery || 'isolated',
|
|
353
|
+
provider: formType === 'script' ? undefined : (formProvider || undefined),
|
|
354
|
+
model: formType === 'script' ? undefined : (formModel || undefined),
|
|
355
|
+
};
|
|
318
356
|
const res = await fetch('/dsh-cron/tasks', {
|
|
319
357
|
method: 'POST',
|
|
320
358
|
headers: { 'Content-Type': 'application/json' },
|
|
321
|
-
body: JSON.stringify(
|
|
322
|
-
title: formTitle,
|
|
323
|
-
schedule: formSchedule,
|
|
324
|
-
prompt: formPrompt,
|
|
325
|
-
provider: formProvider || undefined,
|
|
326
|
-
model: formModel || undefined,
|
|
327
|
-
})
|
|
359
|
+
body: JSON.stringify(payload)
|
|
328
360
|
});
|
|
329
361
|
const data = await res.json();
|
|
330
|
-
if (!data.ok) { alert(data.error || 'Ошибка'); return; }
|
|
362
|
+
if (!data.ok) { alert(data.error || 'Ошибка сохранения'); return; }
|
|
331
363
|
setManualModalOpen(false);
|
|
332
364
|
fetchTasks();
|
|
333
365
|
} catch (err) { alert(err.message); }
|
|
@@ -424,30 +456,57 @@ const React = window.React || require('react');
|
|
|
424
456
|
tasks.length === 0
|
|
425
457
|
? React.createElement('div', { style: { color: '#777', padding: '16px 0', fontSize: '14px' } }, 'Нет запланированных задач')
|
|
426
458
|
: tasks.map((task) =>
|
|
427
|
-
React.createElement('div', {
|
|
459
|
+
React.createElement('div', {
|
|
460
|
+
key: task.id,
|
|
461
|
+
className: 'dsh-cron-task-item',
|
|
462
|
+
onClick: () => openEditModal(task)
|
|
463
|
+
},
|
|
428
464
|
React.createElement('button', {
|
|
465
|
+
type: 'button',
|
|
429
466
|
className: 'dsh-cron-task-status-btn',
|
|
430
467
|
'data-active': task.status === 'active' ? 'true' : undefined,
|
|
431
468
|
title: task.status === 'active' ? 'Приостановить' : 'Возобновить',
|
|
432
|
-
onClick: () => handleToggleTask(task.id)
|
|
469
|
+
onClick: (e) => { e.stopPropagation(); handleToggleTask(task.id); }
|
|
433
470
|
}, React.createElement('span', { dangerouslySetInnerHTML: { __html: task.status === 'active' ? ICON_PAUSE : ICON_PLAY } })),
|
|
434
471
|
React.createElement('div', { className: 'dsh-cron-task-info' },
|
|
435
472
|
React.createElement('div', { className: 'dsh-cron-task-title' },
|
|
436
473
|
task.title,
|
|
474
|
+
React.createElement('span', {
|
|
475
|
+
className: 'dsh-cron-type-tag',
|
|
476
|
+
style: {
|
|
477
|
+
background: task.type === 'script' ? 'rgba(59, 130, 246, 0.15)' : 'rgba(16, 185, 129, 0.15)',
|
|
478
|
+
color: task.type === 'script' ? '#60a5fa' : '#34d399'
|
|
479
|
+
}
|
|
480
|
+
}, task.type === 'script' ? 'Shell' : 'LLM'),
|
|
437
481
|
task.model ? React.createElement('span', { className: 'dsh-cron-task-model-tag' }, task.model.split('/').pop()) : null
|
|
438
482
|
),
|
|
439
483
|
React.createElement('div', { className: 'dsh-cron-task-sched' },
|
|
440
484
|
task.scheduleText || task.schedule,
|
|
441
485
|
task.nextRunAt ? ` · след: ${new Date(task.nextRunAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''
|
|
442
|
-
)
|
|
486
|
+
),
|
|
487
|
+
task.prompt ? React.createElement('div', {
|
|
488
|
+
className: 'dsh-cron-task-prompt-preview',
|
|
489
|
+
title: task.prompt
|
|
490
|
+
}, task.prompt.slice(0, 140) + (task.prompt.length > 140 ? '…' : '')) : null
|
|
443
491
|
),
|
|
444
|
-
React.createElement('div', {
|
|
492
|
+
React.createElement('div', {
|
|
493
|
+
className: 'dsh-cron-task-actions',
|
|
494
|
+
onClick: (e) => e.stopPropagation()
|
|
495
|
+
},
|
|
445
496
|
React.createElement('button', {
|
|
497
|
+
type: 'button',
|
|
446
498
|
className: 'dsh-cron-icon-btn',
|
|
447
499
|
title: 'Запустить немедленно',
|
|
448
500
|
onClick: () => handleRunNow(task.id)
|
|
449
501
|
}, 'Запустить'),
|
|
450
502
|
React.createElement('button', {
|
|
503
|
+
type: 'button',
|
|
504
|
+
className: 'dsh-cron-icon-btn',
|
|
505
|
+
title: 'Редактировать / Подробнее',
|
|
506
|
+
onClick: () => openEditModal(task)
|
|
507
|
+
}, React.createElement('span', { dangerouslySetInnerHTML: { __html: ICON_EDIT } })),
|
|
508
|
+
React.createElement('button', {
|
|
509
|
+
type: 'button',
|
|
451
510
|
className: 'dsh-cron-icon-btn',
|
|
452
511
|
title: 'Удалить задачу',
|
|
453
512
|
onClick: () => handleDelete(task.id)
|
|
@@ -481,30 +540,57 @@ const React = window.React || require('react');
|
|
|
481
540
|
)
|
|
482
541
|
),
|
|
483
542
|
|
|
484
|
-
// 1. Manual creation modal
|
|
543
|
+
// 1. Manual creation / Edit modal
|
|
485
544
|
manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
|
|
486
|
-
React.createElement('div', {
|
|
487
|
-
|
|
545
|
+
React.createElement('div', {
|
|
546
|
+
className: 'dsh-cron-modal',
|
|
547
|
+
style: { maxWidth: '640px' },
|
|
548
|
+
onClick: (e) => e.stopPropagation()
|
|
549
|
+
},
|
|
550
|
+
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|
|
551
|
+
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0 } }, formId ? 'Редактирование задачи' : 'Новая запланированная задача'),
|
|
552
|
+
selectedTaskMeta && React.createElement('span', {
|
|
553
|
+
style: {
|
|
554
|
+
fontSize: '12px',
|
|
555
|
+
padding: '3px 8px',
|
|
556
|
+
borderRadius: '6px',
|
|
557
|
+
background: selectedTaskMeta.status === 'active' ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
|
|
558
|
+
color: selectedTaskMeta.status === 'active' ? '#10b981' : '#ef4444'
|
|
559
|
+
}
|
|
560
|
+
}, selectedTaskMeta.status === 'active' ? '● Активна' : '○ На паузе')
|
|
561
|
+
),
|
|
488
562
|
React.createElement('form', { onSubmit: handleSaveManual },
|
|
489
563
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
490
564
|
React.createElement('label', null, 'Название задачи'),
|
|
491
565
|
React.createElement('input', {
|
|
492
566
|
required: true,
|
|
493
567
|
value: formTitle,
|
|
494
|
-
placeholder: 'Например:
|
|
568
|
+
placeholder: 'Например: dsh-tags-watch',
|
|
495
569
|
onChange: (e) => setFormTitle(e.target.value)
|
|
496
570
|
})
|
|
497
571
|
),
|
|
498
|
-
React.createElement('div', { className: 'dsh-cron-form-
|
|
499
|
-
React.createElement('
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
572
|
+
React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
|
|
573
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
574
|
+
React.createElement('label', null, 'Тип выполнения'),
|
|
575
|
+
React.createElement('select', {
|
|
576
|
+
value: formType,
|
|
577
|
+
onChange: (e) => setFormType(e.target.value)
|
|
578
|
+
},
|
|
579
|
+
React.createElement('option', { value: 'llm' }, 'LLM (Агент с нейросетью)'),
|
|
580
|
+
React.createElement('option', { value: 'script' }, 'NO-LLM (Shell-команда/Скрипт)')
|
|
581
|
+
)
|
|
582
|
+
),
|
|
583
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
584
|
+
React.createElement('label', null, 'Расписание (cron или интервал)'),
|
|
585
|
+
React.createElement('input', {
|
|
586
|
+
required: true,
|
|
587
|
+
value: formSchedule,
|
|
588
|
+
placeholder: '0 */6 * * * или every 2h',
|
|
589
|
+
onChange: (e) => setFormSchedule(e.target.value)
|
|
590
|
+
})
|
|
591
|
+
)
|
|
506
592
|
),
|
|
507
|
-
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
593
|
+
formType !== 'script' && React.createElement('div', { className: 'dsh-cron-form-row', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' } },
|
|
508
594
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
509
595
|
React.createElement('label', null, 'Провайдер LLM'),
|
|
510
596
|
React.createElement('select', {
|
|
@@ -527,14 +613,41 @@ const React = window.React || require('react');
|
|
|
527
613
|
)
|
|
528
614
|
),
|
|
529
615
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
530
|
-
React.createElement('label', null, '
|
|
616
|
+
React.createElement('label', null, formType === 'script' ? 'Shell-команда или скрипт' : 'Инструкция / Промпт для агента'),
|
|
531
617
|
React.createElement('textarea', {
|
|
532
618
|
required: true,
|
|
619
|
+
rows: 9,
|
|
620
|
+
style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
|
|
533
621
|
value: formPrompt,
|
|
534
|
-
placeholder: 'Опишите, что именно агент должен сделать...',
|
|
622
|
+
placeholder: formType === 'script' ? 'curl -fsSL https://... || exit 1' : 'Опишите, что именно агент должен сделать...',
|
|
535
623
|
onChange: (e) => setFormPrompt(e.target.value)
|
|
536
624
|
})
|
|
537
625
|
),
|
|
626
|
+
selectedTaskMeta && React.createElement('div', {
|
|
627
|
+
style: {
|
|
628
|
+
background: 'rgba(255,255,255,0.03)',
|
|
629
|
+
border: '1px solid #2e2e2e',
|
|
630
|
+
borderRadius: '8px',
|
|
631
|
+
padding: '10px 14px',
|
|
632
|
+
fontSize: '12.5px',
|
|
633
|
+
color: '#999',
|
|
634
|
+
marginBottom: '16px',
|
|
635
|
+
display: 'flex',
|
|
636
|
+
justifyContent: 'space-between',
|
|
637
|
+
alignItems: 'center'
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
React.createElement('div', null,
|
|
641
|
+
React.createElement('span', null, 'Следующий запуск: '),
|
|
642
|
+
React.createElement('strong', { style: { color: '#eee' } }, selectedTaskMeta.nextRunAt ? new Date(selectedTaskMeta.nextRunAt).toLocaleString() : 'не запланирован')
|
|
643
|
+
),
|
|
644
|
+
React.createElement('button', {
|
|
645
|
+
type: 'button',
|
|
646
|
+
className: 'dsh-cron-btn-secondary',
|
|
647
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
648
|
+
onClick: () => { handleRunNow(selectedTaskMeta.id); }
|
|
649
|
+
}, 'Запустить сейчас')
|
|
650
|
+
),
|
|
538
651
|
React.createElement('div', { className: 'dsh-cron-modal-foot' },
|
|
539
652
|
React.createElement('button', {
|
|
540
653
|
type: 'button',
|
|
@@ -544,7 +657,7 @@ const React = window.React || require('react');
|
|
|
544
657
|
React.createElement('button', {
|
|
545
658
|
type: 'submit',
|
|
546
659
|
className: 'dsh-cron-btn-primary'
|
|
547
|
-
}, 'Сохранить задачу')
|
|
660
|
+
}, formId ? 'Сохранить изменения' : 'Создать задачу')
|
|
548
661
|
)
|
|
549
662
|
)
|
|
550
663
|
)
|
package/lib/index.js
CHANGED
|
@@ -194,6 +194,7 @@ export function apply(ctx, config) {
|
|
|
194
194
|
schedule: { type: 'string', required: true, description: 'Выражение cron или интервал (every 30m, 0 9 * * 1-5, daily)' },
|
|
195
195
|
prompt: { type: 'string', required: true, description: 'Инструкция для агента при запуске' },
|
|
196
196
|
delivery: { type: 'string', description: 'Режим запуска: current или isolated' },
|
|
197
|
+
type: { type: 'string', description: 'Тип задачи: "llm" (агент с LLM) или "script" (прямой запуск shell без LLM)' },
|
|
197
198
|
provider: { type: 'string', description: 'ID провайдера LLM для выполнения задачи' },
|
|
198
199
|
model: { type: 'string', description: 'ID модели LLM для выполнения задачи' }
|
|
199
200
|
},
|
|
@@ -210,7 +211,7 @@ export function apply(ctx, config) {
|
|
|
210
211
|
return [{ type: 'text', text: value.message }];
|
|
211
212
|
}
|
|
212
213
|
},
|
|
213
|
-
execute: async ({ title, schedule, prompt, delivery, provider, model }) => {
|
|
214
|
+
execute: async ({ title, schedule, prompt, delivery, provider, model, type }) => {
|
|
214
215
|
try {
|
|
215
216
|
const parsed = parseScheduleExpression(schedule);
|
|
216
217
|
const task = store.set({
|
|
@@ -218,6 +219,7 @@ export function apply(ctx, config) {
|
|
|
218
219
|
schedule: parsed.cronPattern,
|
|
219
220
|
scheduleText: parsed.humanText,
|
|
220
221
|
prompt,
|
|
222
|
+
type: type || (provider || model ? 'llm' : 'llm'),
|
|
221
223
|
delivery: delivery || 'isolated',
|
|
222
224
|
provider: provider || undefined,
|
|
223
225
|
model: model || undefined,
|
package/lib/prompt.js
CHANGED
|
@@ -5,13 +5,46 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export function buildAgentCronPrompt(userText) {
|
|
7
7
|
const text = (userText || '').trim();
|
|
8
|
-
return `Пользователь хочет
|
|
8
|
+
return `Пользователь хочет настроить запланированную задачу (cron) через DSH Cron.
|
|
9
|
+
Исходный запрос пользователя:
|
|
9
10
|
"${text}"
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
1.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
ВАЖНЕЙШИЕ ПРАВИЛА ДЛЯ АГЕНТА:
|
|
13
|
+
1. СТРОГИЙ ЗАПРЕТ НА СПЕШКУ:
|
|
14
|
+
КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вызывать инструмент \`cron_schedule_task\` в первом же ответе!
|
|
15
|
+
Ты НЕ имеешь права создавать задачу, пока не задашь пользователю уточняющие вопросы и не получишь его подтверждение.
|
|
16
|
+
|
|
17
|
+
2. ЧТО ТЫ ОБЯЗАН УТОЧНИТЬ В ПЕРВОМ ОТВЕТЕ:
|
|
18
|
+
- Тип задачи (LLM или NO-LLM?):
|
|
19
|
+
* LLM-крон: агент запускается по расписанию с языковой моделью, анализирует вывод, делает выводы, парсит неструктурированный текст (расходует токены).
|
|
20
|
+
* NO-LLM крон (скрипт/bash): периодический запуск shell-команды или скрипта (curl, git check, ping, проверка системных служб) БЕЗ участия LLM. Не расходует токены, сообщает только при ненулевом коде возврата или ошибке.
|
|
21
|
+
Спроси пользователя, какой вариант предпочтительнее для его задачи (или аргументированно предложи подходящий).
|
|
22
|
+
- Выбор модели (если выбран LLM-крон):
|
|
23
|
+
* Предложи экономичную и быструю модель (например, \`deepseek/deepseek-v4-flash\` или \`Qwen/Qwen3.8-Flash\`).
|
|
24
|
+
* Спроси, подходит ли эта модель или пользователь хочет использовать другую подключенную модель.
|
|
25
|
+
- Точное расписание (schedule):
|
|
26
|
+
* Предложи конкретное выражение cron (например, "0 */6 * * *" для 4 раз в день или "0 9 * * 1-5" по будням) и понятное текстовое пояснение.
|
|
27
|
+
* Уточни, устраивает ли такое расписание.
|
|
28
|
+
- Silent Rule (Правило тишины / Уведомления):
|
|
29
|
+
* По умолчанию для регулярных проверок действует Silent Rule: если всё в порядке / нет обновлений / сервис работает штатно — агент молчит или возвращает пустой ответ, чтобы не спамить. Оповещение отправляется ТОЛЬКО при обнаружении изменений, обновлений или проблем.
|
|
30
|
+
* Уточни, нужно ли применять Silent Rule или присылать отчёт при каждом запуске.
|
|
31
|
+
|
|
32
|
+
3. СТРУКТУРА ПЕРВОГО ОТВЕТА:
|
|
33
|
+
- Кратко подтверди суть задачи.
|
|
34
|
+
- Сформулируй проект карточки задачи:
|
|
35
|
+
• Тип: [LLM / NO-LLM]
|
|
36
|
+
• Расписание: [cron-выражение и понятное описание]
|
|
37
|
+
• Модель: [если LLM — предлагаемая модель]
|
|
38
|
+
• Логика проверки и Silent Rule: [кратко]
|
|
39
|
+
- Задай недостающие вопросы (LLM/NO-LLM, модель, расписание, порог алерта).
|
|
40
|
+
- Попроси пользователя подтвердить или скорректировать параметры перед созданием.
|
|
41
|
+
|
|
42
|
+
4. СОЗДАНИЕ ЗАДАЧИ:
|
|
43
|
+
Только ПОСЛЕ ТОГО, как пользователь ответит и подтвердит параметры, вызови инструмент \`cron_schedule_task\`, передав:
|
|
44
|
+
- title: понятное название задачи
|
|
45
|
+
- schedule: согласованное cron-выражение
|
|
46
|
+
- prompt: финальная подробная инструкция для агента (с обязательным Silent Rule) ИЛИ shell-команда для NO-LLM
|
|
47
|
+
- type: "llm" или "script"
|
|
48
|
+
- provider и model: согласованный провайдер и модель (если type="llm")
|
|
49
|
+
- delivery: "isolated"`;
|
|
50
|
+
}
|
package/lib/runner.js
CHANGED
|
@@ -6,10 +6,28 @@ export class SessionRunner {
|
|
|
6
6
|
this.ctx = ctx;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
async execute(task) {
|
|
10
10
|
console.log(`[dsh-cron] executing task "${task.title}" (${task.id})`);
|
|
11
11
|
|
|
12
|
-
// 1.
|
|
12
|
+
// 1. Direct shell / script task fallback (NO-LLM)
|
|
13
|
+
if (task.type === 'script' || task.mode === 'no-llm') {
|
|
14
|
+
const { exec } = await import('node:child_process');
|
|
15
|
+
const { promisify } = await import('node:util');
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
try {
|
|
18
|
+
const { stdout, stderr } = await execAsync(task.prompt, {
|
|
19
|
+
timeout: 60000,
|
|
20
|
+
cwd: process.cwd()
|
|
21
|
+
});
|
|
22
|
+
const out = (stdout || stderr || '').trim();
|
|
23
|
+
return out || 'Script completed with code 0';
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error(`[dsh-cron] script task failed:`, err.message);
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. If ctx.agents is available, run through agent session (LLM)
|
|
13
31
|
if (this.ctx && this.ctx.agents && typeof this.ctx.agents.create === 'function') {
|
|
14
32
|
try {
|
|
15
33
|
const { randomUUID } = await import('node:crypto');
|
|
@@ -34,9 +52,10 @@ export class SessionRunner {
|
|
|
34
52
|
const provider = task.provider || defaultSel?.provider;
|
|
35
53
|
const model = task.model || defaultSel?.model;
|
|
36
54
|
|
|
55
|
+
const cwd = process.cwd();
|
|
37
56
|
const handle = await this.ctx.agents.create({
|
|
38
57
|
sessionId: mintSessionId(),
|
|
39
|
-
meta: {},
|
|
58
|
+
meta: { cwd },
|
|
40
59
|
agentOptions: provider && model ? { provider, model } : undefined,
|
|
41
60
|
});
|
|
42
61
|
|
|
@@ -53,7 +72,7 @@ export class SessionRunner {
|
|
|
53
72
|
}
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
//
|
|
75
|
+
// 3. Check if legacy session service is present
|
|
57
76
|
if (this.ctx && this.ctx.session && typeof this.ctx.session.create === 'function') {
|
|
58
77
|
try {
|
|
59
78
|
const session = await this.ctx.session.create({
|
|
@@ -71,15 +90,6 @@ export class SessionRunner {
|
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
92
|
|
|
74
|
-
// 3. Direct shell / script task fallback
|
|
75
|
-
if (task.type === 'script' && task.prompt) {
|
|
76
|
-
const { exec } = await import('node:child_process');
|
|
77
|
-
const { promisify } = await import('node:util');
|
|
78
|
-
const execAsync = promisify(exec);
|
|
79
|
-
const { stdout, stderr } = await execAsync(task.prompt, { timeout: 60000 });
|
|
80
|
-
return stdout || stderr || 'Script completed with code 0';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
93
|
return `[dsh-cron] Prompt triggered: "${task.prompt.slice(0, 100)}..."`;
|
|
84
94
|
}
|
|
85
95
|
}
|
package/lib/store.js
CHANGED
|
@@ -82,21 +82,26 @@ export class TaskStore {
|
|
|
82
82
|
set(task) {
|
|
83
83
|
const now = Date.now();
|
|
84
84
|
const id = task.id || ('cron_' + Math.random().toString(36).slice(2, 9));
|
|
85
|
+
const prev = this.tasks.get(id) || {};
|
|
85
86
|
const record = {
|
|
87
|
+
...prev,
|
|
88
|
+
...task,
|
|
86
89
|
id,
|
|
87
|
-
title: task.title || 'Новая задача',
|
|
88
|
-
schedule: task.schedule || '0 9 * * *',
|
|
89
|
-
scheduleText: task.scheduleText || task.schedule,
|
|
90
|
-
prompt: task.prompt || '',
|
|
91
|
-
type: task.type || '
|
|
92
|
-
status: task.status || 'active',
|
|
93
|
-
delivery: task.delivery || '
|
|
94
|
-
|
|
90
|
+
title: task.title !== undefined ? task.title : (prev.title || 'Новая задача'),
|
|
91
|
+
schedule: task.schedule !== undefined ? task.schedule : (prev.schedule || '0 9 * * *'),
|
|
92
|
+
scheduleText: task.scheduleText !== undefined ? task.scheduleText : (prev.scheduleText || task.schedule),
|
|
93
|
+
prompt: task.prompt !== undefined ? task.prompt : (prev.prompt || ''),
|
|
94
|
+
type: task.type !== undefined ? task.type : (prev.type || 'llm'),
|
|
95
|
+
status: task.status !== undefined ? task.status : (prev.status || 'active'),
|
|
96
|
+
delivery: task.delivery !== undefined ? task.delivery : (prev.delivery || 'isolated'),
|
|
97
|
+
provider: task.provider !== undefined ? task.provider : prev.provider,
|
|
98
|
+
model: task.model !== undefined ? task.model : prev.model,
|
|
99
|
+
createdAt: prev.createdAt || task.createdAt || now,
|
|
95
100
|
updatedAt: now,
|
|
96
|
-
lastRunAt: task.lastRunAt || null,
|
|
97
|
-
lastStatus: task.lastStatus || null,
|
|
98
|
-
lastDurationMs: task.lastDurationMs || 0,
|
|
99
|
-
nextRunAt: task.nextRunAt || null,
|
|
101
|
+
lastRunAt: task.lastRunAt !== undefined ? task.lastRunAt : (prev.lastRunAt || null),
|
|
102
|
+
lastStatus: task.lastStatus !== undefined ? task.lastStatus : (prev.lastStatus || null),
|
|
103
|
+
lastDurationMs: task.lastDurationMs !== undefined ? task.lastDurationMs : (prev.lastDurationMs || 0),
|
|
104
|
+
nextRunAt: task.nextRunAt !== undefined ? task.nextRunAt : (prev.nextRunAt || null),
|
|
100
105
|
};
|
|
101
106
|
this.tasks.set(id, record);
|
|
102
107
|
this.save();
|