@goodandready/dsh-cron 0.1.20 → 0.1.21
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 +205 -27
- package/lib/index.js +187 -14
- package/lib/prompt.js +2 -2
- package/lib/runner.js +15 -6
- package/lib/scheduler.js +15 -1
- package/lib/store.js +30 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -268,7 +268,9 @@ const React = window.React || require('react');
|
|
|
268
268
|
const [settingsChatId, setSettingsChatId] = React.useState('');
|
|
269
269
|
const [settingsNotifyGlobal, setSettingsNotifyGlobal] = React.useState(false);
|
|
270
270
|
const [settingsOnlyOnFailureGlobal, setSettingsOnlyOnFailureGlobal] = React.useState(false);
|
|
271
|
+
const [settingsKanbanUrl, setSettingsKanbanUrl] = React.useState('http://127.0.0.1:3000');
|
|
271
272
|
const [settingsHasDefault, setSettingsHasDefault] = React.useState(false);
|
|
273
|
+
const [settingsHasCustomBotToken, setSettingsHasCustomBotToken] = React.useState(false);
|
|
272
274
|
const [testStatus, setTestStatus] = React.useState(null);
|
|
273
275
|
|
|
274
276
|
// DSH dialog state
|
|
@@ -283,40 +285,55 @@ const React = window.React || require('react');
|
|
|
283
285
|
|
|
284
286
|
React.useEffect(() => {
|
|
285
287
|
const onKey = (e) => {
|
|
286
|
-
if (e.key
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
if (e.key === 'Escape') {
|
|
289
|
+
if (dropdownOpen) { setDropdownOpen(false); return; }
|
|
290
|
+
if (manualModalOpen) { setManualModalOpen(false); return; }
|
|
291
|
+
if (settingsModalOpen) { setSettingsModalOpen(false); return; }
|
|
292
|
+
if (dshModalOpen && !dshLoading) { setDshModalOpen(false); return; }
|
|
293
|
+
if (onClose) onClose();
|
|
294
|
+
else toggle.set(false);
|
|
295
|
+
}
|
|
292
296
|
};
|
|
293
|
-
|
|
294
|
-
return () =>
|
|
297
|
+
window.addEventListener('keydown', onKey);
|
|
298
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
295
299
|
}, [manualModalOpen, settingsModalOpen, dshModalOpen, dropdownOpen, onClose]);
|
|
296
300
|
|
|
297
301
|
const fetchTasks = async () => {
|
|
302
|
+
setLoading(true);
|
|
298
303
|
try {
|
|
299
|
-
const res = await fetch('/dsh-cron/tasks?status=' + tab + '&query=' + encodeURIComponent(query));
|
|
304
|
+
const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query));
|
|
300
305
|
const data = await res.json();
|
|
301
306
|
if (data && data.ok) {
|
|
302
307
|
setTasks(data.tasks || []);
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
setRecommendations(data.recommendations || []);
|
|
309
|
+
setStats(data.stats || null);
|
|
305
310
|
}
|
|
306
|
-
} catch (err) {
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.error('[dsh-cron] fetch tasks error:', err);
|
|
313
|
+
} finally {
|
|
314
|
+
setLoading(false);
|
|
315
|
+
}
|
|
307
316
|
};
|
|
308
317
|
|
|
309
|
-
const loadModels = async (
|
|
318
|
+
const loadModels = async (providerName) => {
|
|
310
319
|
try {
|
|
311
|
-
const url = '/dsh-cron/models
|
|
320
|
+
const url = providerName ? '/dsh-cron/models?provider=' + encodeURIComponent(providerName) : '/dsh-cron/models';
|
|
312
321
|
const res = await fetch(url);
|
|
313
322
|
const data = await res.json();
|
|
314
|
-
if (data) {
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
323
|
+
if (data && data.ok) {
|
|
324
|
+
if (!providerName) {
|
|
325
|
+
setProviders(data.providers || []);
|
|
326
|
+
setDefaultModelInfo(data.defaultModel || null);
|
|
327
|
+
if (data.defaultModel?.models) {
|
|
328
|
+
setModels(data.defaultModel.models);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
setModels(data.models || []);
|
|
332
|
+
}
|
|
318
333
|
}
|
|
319
|
-
} catch (err) {
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error('[dsh-cron] load models error:', err);
|
|
336
|
+
}
|
|
320
337
|
};
|
|
321
338
|
|
|
322
339
|
React.useEffect(() => { fetchTasks(); }, [tab, query]);
|
|
@@ -324,14 +341,14 @@ const React = window.React || require('react');
|
|
|
324
341
|
|
|
325
342
|
const handleToggleTask = async (id) => {
|
|
326
343
|
try {
|
|
327
|
-
await fetch('/dsh-cron/
|
|
344
|
+
await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/toggle', { method: 'POST' });
|
|
328
345
|
fetchTasks();
|
|
329
346
|
} catch (err) {}
|
|
330
347
|
};
|
|
331
348
|
|
|
332
349
|
const handleRunNow = async (id) => {
|
|
333
350
|
try {
|
|
334
|
-
await fetch('/dsh-cron/
|
|
351
|
+
await fetch('/dsh-cron/tasks/' + encodeURIComponent(id) + '/run', { method: 'POST' });
|
|
335
352
|
alert('Задача запущена');
|
|
336
353
|
fetchTasks();
|
|
337
354
|
} catch (err) {}
|
|
@@ -340,7 +357,7 @@ const React = window.React || require('react');
|
|
|
340
357
|
const handleDelete = async (id) => {
|
|
341
358
|
if (!confirm('Удалить эту задачу?')) return;
|
|
342
359
|
try {
|
|
343
|
-
await fetch('/dsh-cron/
|
|
360
|
+
await fetch('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
344
361
|
fetchTasks();
|
|
345
362
|
} catch (err) {}
|
|
346
363
|
};
|
|
@@ -356,7 +373,9 @@ const React = window.React || require('react');
|
|
|
356
373
|
setSettingsChatId(data.settings.chatId || '');
|
|
357
374
|
setSettingsNotifyGlobal(Boolean(data.settings.notifyTelegram));
|
|
358
375
|
setSettingsOnlyOnFailureGlobal(Boolean(data.settings.onlyOnFailure));
|
|
376
|
+
setSettingsKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
359
377
|
setSettingsHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
378
|
+
setSettingsHasCustomBotToken(Boolean(data.settings.hasCustomBotToken));
|
|
360
379
|
}
|
|
361
380
|
} catch (e) {}
|
|
362
381
|
};
|
|
@@ -371,7 +390,8 @@ const React = window.React || require('react');
|
|
|
371
390
|
botToken: settingsBotToken,
|
|
372
391
|
chatId: settingsChatId,
|
|
373
392
|
notifyTelegram: settingsNotifyGlobal,
|
|
374
|
-
onlyOnFailure: settingsOnlyOnFailureGlobal
|
|
393
|
+
onlyOnFailure: settingsOnlyOnFailureGlobal,
|
|
394
|
+
kanbanBaseUrl: settingsKanbanUrl,
|
|
375
395
|
})
|
|
376
396
|
});
|
|
377
397
|
const data = await res.json();
|
|
@@ -439,7 +459,7 @@ const React = window.React || require('react');
|
|
|
439
459
|
const loadTaskHistory = async (taskId) => {
|
|
440
460
|
setHistoryLoading(true);
|
|
441
461
|
try {
|
|
442
|
-
const res = await fetch('/dsh-cron/
|
|
462
|
+
const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/history');
|
|
443
463
|
const data = await res.json();
|
|
444
464
|
if (data.ok) {
|
|
445
465
|
setTaskHistory(data.history || []);
|
|
@@ -486,6 +506,7 @@ const React = window.React || require('react');
|
|
|
486
506
|
try {
|
|
487
507
|
const payload = {
|
|
488
508
|
id: formId || undefined,
|
|
509
|
+
status: selectedTaskMeta ? selectedTaskMeta.status : 'active',
|
|
489
510
|
title: formTitle,
|
|
490
511
|
schedule: formSchedule,
|
|
491
512
|
prompt: formPrompt,
|
|
@@ -1094,6 +1115,93 @@ const React = window.React || require('react');
|
|
|
1094
1115
|
|
|
1095
1116
|
function CronSettingsCard(props) {
|
|
1096
1117
|
const { ctx, toggle } = props;
|
|
1118
|
+
const [token, setToken] = React.useState('');
|
|
1119
|
+
const [chatId, setChatId] = React.useState('');
|
|
1120
|
+
const [notify, setNotify] = React.useState(false);
|
|
1121
|
+
const [onlyFailure, setOnlyFailure] = React.useState(false);
|
|
1122
|
+
const [kanbanUrl, setKanbanUrl] = React.useState('http://127.0.0.1:3000');
|
|
1123
|
+
const [hasDefault, setHasDefault] = React.useState(false);
|
|
1124
|
+
const [saved, setSaved] = React.useState(false);
|
|
1125
|
+
const [testInfo, setTestInfo] = React.useState(null);
|
|
1126
|
+
|
|
1127
|
+
React.useEffect(() => {
|
|
1128
|
+
let active = true;
|
|
1129
|
+
fetch('/dsh-cron/settings')
|
|
1130
|
+
.then(r => r.json())
|
|
1131
|
+
.then(data => {
|
|
1132
|
+
if (active && data && data.ok && data.settings) {
|
|
1133
|
+
setToken(data.settings.botToken || '');
|
|
1134
|
+
setChatId(data.settings.chatId || '');
|
|
1135
|
+
setNotify(Boolean(data.settings.notifyTelegram));
|
|
1136
|
+
setOnlyFailure(Boolean(data.settings.onlyOnFailure));
|
|
1137
|
+
setKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
1138
|
+
setHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
1139
|
+
}
|
|
1140
|
+
})
|
|
1141
|
+
.catch(() => {});
|
|
1142
|
+
return () => { active = false; };
|
|
1143
|
+
}, []);
|
|
1144
|
+
|
|
1145
|
+
const saveSettings = async (e) => {
|
|
1146
|
+
if (e) e.preventDefault();
|
|
1147
|
+
try {
|
|
1148
|
+
const res = await fetch('/dsh-cron/settings', {
|
|
1149
|
+
method: 'POST',
|
|
1150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1151
|
+
body: JSON.stringify({
|
|
1152
|
+
botToken: token,
|
|
1153
|
+
chatId,
|
|
1154
|
+
notifyTelegram: notify,
|
|
1155
|
+
onlyOnFailure: onlyFailure,
|
|
1156
|
+
kanbanBaseUrl: kanbanUrl
|
|
1157
|
+
})
|
|
1158
|
+
});
|
|
1159
|
+
const data = await res.json();
|
|
1160
|
+
if (data && data.ok) {
|
|
1161
|
+
setSaved(true);
|
|
1162
|
+
setTimeout(() => setSaved(false), 2500);
|
|
1163
|
+
}
|
|
1164
|
+
} catch (err) {}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
const testTelegram = async () => {
|
|
1168
|
+
setTestInfo({ loading: true, message: 'Отправка...' });
|
|
1169
|
+
try {
|
|
1170
|
+
const res = await fetch('/dsh-cron/telegram/test', {
|
|
1171
|
+
method: 'POST',
|
|
1172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1173
|
+
body: JSON.stringify({ botToken: token, chatId })
|
|
1174
|
+
});
|
|
1175
|
+
const data = await res.json();
|
|
1176
|
+
if (data && data.ok) {
|
|
1177
|
+
setTestInfo({ success: true, message: '✅ Успешно доставлено в Telegram' });
|
|
1178
|
+
} else {
|
|
1179
|
+
setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Сбой отправки') });
|
|
1180
|
+
}
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
setTestInfo({ success: false, message: '❌ ' + err.message });
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
const testKanban = async () => {
|
|
1187
|
+
setTestInfo({ loading: true, message: 'Проверка Kanban...' });
|
|
1188
|
+
try {
|
|
1189
|
+
const res = await fetch('/dsh-cron/kanban/test', {
|
|
1190
|
+
method: 'POST',
|
|
1191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1192
|
+
body: JSON.stringify({ kanbanBaseUrl: kanbanUrl })
|
|
1193
|
+
});
|
|
1194
|
+
const data = await res.json();
|
|
1195
|
+
if (data && data.ok) {
|
|
1196
|
+
setTestInfo({ success: true, message: '✅ Карточка в Kanban создана' });
|
|
1197
|
+
} else {
|
|
1198
|
+
setTestInfo({ success: false, message: '❌ ' + (data?.error || 'Ошибка Kanban') });
|
|
1199
|
+
}
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
setTestInfo({ success: false, message: '❌ ' + err.message });
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1097
1205
|
return React.createElement('div', { className: 'dsh-cron-card' },
|
|
1098
1206
|
React.createElement('div', { className: 'dsh-cron-card-head' },
|
|
1099
1207
|
React.createElement('span', { className: 'dsh-cron-card-title' }, '⏰ Планировщик задач (Cron)'),
|
|
@@ -1101,10 +1209,80 @@ const React = window.React || require('react');
|
|
|
1101
1209
|
type: 'button',
|
|
1102
1210
|
className: 'dsh-cron-btn-primary',
|
|
1103
1211
|
onClick: () => { if (toggle) toggle.set(true); }
|
|
1104
|
-
}, 'Открыть
|
|
1212
|
+
}, 'Открыть панель задач')
|
|
1213
|
+
),
|
|
1214
|
+
React.createElement('div', { className: 'dsh-cron-card-desc', style: { marginBottom: '16px' } },
|
|
1215
|
+
'Автоматическое выполнение задач по расписанию (cron/интервалы), фоновые сценарии агентов и интеграция с Telegram и Kanban.'
|
|
1105
1216
|
),
|
|
1106
|
-
React.createElement('
|
|
1107
|
-
'
|
|
1217
|
+
React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
|
|
1218
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
|
|
1219
|
+
React.createElement('label', null, 'Telegram Bot Token'),
|
|
1220
|
+
React.createElement('input', {
|
|
1221
|
+
type: 'password',
|
|
1222
|
+
value: token,
|
|
1223
|
+
placeholder: hasDefault ? 'Используется бот из настроек DSH (или введите свой)' : '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
|
|
1224
|
+
onChange: (e) => setToken(e.target.value)
|
|
1225
|
+
}),
|
|
1226
|
+
hasDefault && React.createElement('div', { className: 'dsh-cron-hint' }, '✓ Найден настроенный бот в профиле DSH')
|
|
1227
|
+
),
|
|
1228
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
|
|
1229
|
+
React.createElement('label', null, 'Telegram Chat ID'),
|
|
1230
|
+
React.createElement('input', {
|
|
1231
|
+
value: chatId,
|
|
1232
|
+
placeholder: 'Например: 345678901',
|
|
1233
|
+
onChange: (e) => setChatId(e.target.value)
|
|
1234
|
+
})
|
|
1235
|
+
),
|
|
1236
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
|
|
1237
|
+
React.createElement('label', null, 'Kanban Base URL'),
|
|
1238
|
+
React.createElement('input', {
|
|
1239
|
+
value: kanbanUrl,
|
|
1240
|
+
placeholder: 'http://127.0.0.1:3000',
|
|
1241
|
+
onChange: (e) => setKanbanUrl(e.target.value)
|
|
1242
|
+
})
|
|
1243
|
+
),
|
|
1244
|
+
React.createElement('div', { style: { background: 'rgba(255,255,255,0.02)', border: '1px solid #2e2e2e', borderRadius: '6px', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
1245
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
1246
|
+
React.createElement('input', {
|
|
1247
|
+
type: 'checkbox',
|
|
1248
|
+
checked: notify,
|
|
1249
|
+
onChange: (e) => setNotify(e.target.checked)
|
|
1250
|
+
}),
|
|
1251
|
+
React.createElement('span', null, 'Глобально отправлять отчеты всех задач в Telegram')
|
|
1252
|
+
),
|
|
1253
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
1254
|
+
React.createElement('input', {
|
|
1255
|
+
type: 'checkbox',
|
|
1256
|
+
checked: onlyFailure,
|
|
1257
|
+
onChange: (e) => setOnlyFailure(e.target.checked)
|
|
1258
|
+
}),
|
|
1259
|
+
React.createElement('span', null, 'Уведомлять только при ошибках')
|
|
1260
|
+
)
|
|
1261
|
+
),
|
|
1262
|
+
testInfo && testInfo.message && React.createElement('div', {
|
|
1263
|
+
className: 'dsh-cron-status-banner',
|
|
1264
|
+
'data-type': testInfo.success ? 'success' : 'error',
|
|
1265
|
+
style: { margin: 0, padding: '8px 12px' }
|
|
1266
|
+
}, testInfo.message),
|
|
1267
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginTop: '4px' } },
|
|
1268
|
+
React.createElement('button', {
|
|
1269
|
+
type: 'submit',
|
|
1270
|
+
className: 'dsh-cron-btn-primary',
|
|
1271
|
+
style: { minWidth: '140px' }
|
|
1272
|
+
}, saved ? '✓ Сохранено' : 'Сохранить настройки'),
|
|
1273
|
+
React.createElement('button', {
|
|
1274
|
+
type: 'button',
|
|
1275
|
+
className: 'dsh-cron-btn-secondary',
|
|
1276
|
+
onClick: testTelegram,
|
|
1277
|
+
disabled: testInfo?.loading
|
|
1278
|
+
}, '🔔 Тест Telegram'),
|
|
1279
|
+
React.createElement('button', {
|
|
1280
|
+
type: 'button',
|
|
1281
|
+
className: 'dsh-cron-btn-secondary',
|
|
1282
|
+
onClick: testKanban,
|
|
1283
|
+
disabled: testInfo?.loading
|
|
1284
|
+
}, '📋 Тест Kanban')
|
|
1285
|
+
)
|
|
1108
1286
|
)
|
|
1109
1287
|
);
|
|
1110
1288
|
}
|
package/lib/index.js
CHANGED
|
@@ -7,10 +7,19 @@ import { sendTelegramMessage } from './telegram.js';
|
|
|
7
7
|
import { createKanbanCard } from './integrations.js';
|
|
8
8
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import z from '@deepseek-ai/schemastery';
|
|
10
11
|
|
|
11
12
|
export const name = '@goodandready/dsh-cron';
|
|
12
13
|
export const inject = ['tools', 'webServer', 'settings', 'llm', 'agents', 'agentDefaultModel'];
|
|
13
14
|
|
|
15
|
+
export const Config = z.object({
|
|
16
|
+
botToken: z.string().role('secret').default('').description('Telegram Bot Token для отправки отчетов'),
|
|
17
|
+
chatId: z.string().default('').description('Telegram Chat ID получателя'),
|
|
18
|
+
notifyTelegram: z.boolean().default(false).description('Включить глобальную отправку отчетов в Telegram'),
|
|
19
|
+
onlyOnFailure: z.boolean().default(false).description('Отправлять отчеты в Telegram только при ошибках'),
|
|
20
|
+
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('Базовый URL Kanban API'),
|
|
21
|
+
});
|
|
22
|
+
|
|
14
23
|
function parseJsonBody(req) {
|
|
15
24
|
return new Promise((resolve, reject) => {
|
|
16
25
|
let data = '';
|
|
@@ -36,6 +45,24 @@ export function apply(ctx, config) {
|
|
|
36
45
|
const runner = new SessionRunner(ctx);
|
|
37
46
|
const scheduler = new TaskScheduler(store, (task, opts) => runner.execute(task, opts));
|
|
38
47
|
|
|
48
|
+
// Register settings in Harness settings service with Config schema and initial state from store (#71)
|
|
49
|
+
ctx.inject(['settings'], (sctx) => {
|
|
50
|
+
try {
|
|
51
|
+
const scope = sctx.settings?.register?.('@goodandready/dsh-cron', Config, {
|
|
52
|
+
base: store.getSettings()
|
|
53
|
+
});
|
|
54
|
+
if (scope && typeof scope.watch === 'function') {
|
|
55
|
+
scope.watch((updated) => {
|
|
56
|
+
if (updated && typeof updated === 'object') {
|
|
57
|
+
store.saveSettings(updated);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.warn('[dsh-cron] settings registration warning:', e.message);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
39
66
|
scheduler.start();
|
|
40
67
|
|
|
41
68
|
ctx.effect(() => {
|
|
@@ -98,25 +125,33 @@ export function apply(ctx, config) {
|
|
|
98
125
|
return;
|
|
99
126
|
}
|
|
100
127
|
const parsed = parseScheduleExpression(body.schedule);
|
|
128
|
+
const current = body.id ? store.get(body.id) : null;
|
|
129
|
+
const resolvedStatus = body.status || (current ? current.status : 'active');
|
|
130
|
+
const taskType = body.type === 'script' ? 'script' : 'llm';
|
|
131
|
+
|
|
101
132
|
const task = store.set({
|
|
102
133
|
id: body.id,
|
|
103
134
|
title: body.title,
|
|
104
135
|
schedule: parsed.cronPattern || body.schedule,
|
|
105
136
|
scheduleText: body.scheduleText || parsed.humanText,
|
|
106
137
|
prompt: body.prompt,
|
|
107
|
-
type:
|
|
138
|
+
type: taskType,
|
|
108
139
|
delivery: body.delivery || 'current',
|
|
109
|
-
status:
|
|
140
|
+
status: resolvedStatus,
|
|
110
141
|
provider: body.provider || undefined,
|
|
111
142
|
model: body.model || undefined,
|
|
112
|
-
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : false,
|
|
113
|
-
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : false,
|
|
114
|
-
timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : 1800,
|
|
115
|
-
overlapPolicy: body.overlapPolicy || 'skip',
|
|
116
|
-
kanbanMode: body.kanbanMode || 'none',
|
|
143
|
+
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : (current ? current.notifyTelegram : false),
|
|
144
|
+
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : (current ? current.onlyOnFailure : false),
|
|
145
|
+
timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : (current ? current.timeoutSeconds : 1800),
|
|
146
|
+
overlapPolicy: body.overlapPolicy || (current ? current.overlapPolicy : 'skip'),
|
|
147
|
+
kanbanMode: body.kanbanMode || (current ? current.kanbanMode : 'none'),
|
|
117
148
|
oneShot: Boolean(parsed.isOneShot || body.oneShot),
|
|
118
149
|
});
|
|
119
|
-
|
|
150
|
+
if (task.status === 'active') {
|
|
151
|
+
scheduler.scheduleTask(task);
|
|
152
|
+
} else {
|
|
153
|
+
scheduler.pauseTask(task.id);
|
|
154
|
+
}
|
|
120
155
|
sendJson(res, 200, { ok: true, task });
|
|
121
156
|
return;
|
|
122
157
|
}
|
|
@@ -153,13 +188,13 @@ export function apply(ctx, config) {
|
|
|
153
188
|
handler: async (req, res) => {
|
|
154
189
|
try {
|
|
155
190
|
if (req.method === 'GET') {
|
|
156
|
-
sendJson(res, 200, { ok: true, settings: store.
|
|
191
|
+
sendJson(res, 200, { ok: true, settings: store.getClientSettings() });
|
|
157
192
|
return;
|
|
158
193
|
}
|
|
159
194
|
if (req.method === 'POST') {
|
|
160
195
|
const body = await parseJsonBody(req);
|
|
161
|
-
|
|
162
|
-
sendJson(res, 200, { ok: true, settings:
|
|
196
|
+
store.saveSettings(body);
|
|
197
|
+
sendJson(res, 200, { ok: true, settings: store.getClientSettings() });
|
|
163
198
|
return;
|
|
164
199
|
}
|
|
165
200
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
@@ -282,6 +317,17 @@ export function apply(ctx, config) {
|
|
|
282
317
|
return;
|
|
283
318
|
}
|
|
284
319
|
|
|
320
|
+
if (req.method === 'POST' && (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle'))) {
|
|
321
|
+
const current = store.get(id);
|
|
322
|
+
if (!current) {
|
|
323
|
+
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const task = scheduler.toggleTask(id);
|
|
327
|
+
sendJson(res, 200, { ok: true, task });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
285
331
|
if (req.method === 'PATCH') {
|
|
286
332
|
const body = await parseJsonBody(req);
|
|
287
333
|
const current = store.get(id);
|
|
@@ -289,15 +335,24 @@ export function apply(ctx, config) {
|
|
|
289
335
|
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
290
336
|
return;
|
|
291
337
|
}
|
|
292
|
-
|
|
338
|
+
let patchData = { ...body };
|
|
339
|
+
if (body.schedule) {
|
|
340
|
+
const parsed = parseScheduleExpression(body.schedule);
|
|
341
|
+
patchData.schedule = parsed.cronPattern || body.schedule;
|
|
342
|
+
patchData.scheduleText = body.scheduleText || parsed.humanText;
|
|
343
|
+
patchData.oneShot = Boolean(parsed.isOneShot || body.oneShot);
|
|
344
|
+
}
|
|
345
|
+
const task = store.set({ ...current, ...patchData, id });
|
|
293
346
|
if (task.status === 'active') {
|
|
294
347
|
scheduler.scheduleTask(task);
|
|
348
|
+
} else {
|
|
349
|
+
scheduler.pauseTask(id);
|
|
295
350
|
}
|
|
296
351
|
sendJson(res, 200, { ok: true, task });
|
|
297
352
|
return;
|
|
298
353
|
}
|
|
299
354
|
|
|
300
|
-
if (req.method === 'DELETE') {
|
|
355
|
+
if (req.method === 'DELETE' || (req.method === 'POST' && action === 'delete')) {
|
|
301
356
|
const current = store.get(id);
|
|
302
357
|
if (current) {
|
|
303
358
|
scheduler.pauseTask(id);
|
|
@@ -314,6 +369,64 @@ export function apply(ctx, config) {
|
|
|
314
369
|
}
|
|
315
370
|
}), 'dsh-cron: /tasks/:id');
|
|
316
371
|
|
|
372
|
+
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
373
|
+
ctx.effect(() => ctx.webServer.register({
|
|
374
|
+
kind: 'prefix',
|
|
375
|
+
path: '/dsh-cron/action/',
|
|
376
|
+
handler: async (req, res) => {
|
|
377
|
+
try {
|
|
378
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
379
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
380
|
+
const id = parts[2];
|
|
381
|
+
const action = parts[3];
|
|
382
|
+
|
|
383
|
+
if (!id) {
|
|
384
|
+
sendJson(res, 400, { ok: false, error: 'ID задачи не указан' });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (action === 'history' || (req.method === 'GET' && !action)) {
|
|
389
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
390
|
+
const history = store.getHistory(id, limit);
|
|
391
|
+
sendJson(res, 200, { ok: true, history });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (action === 'run') {
|
|
396
|
+
await scheduler.triggerManualRun(id);
|
|
397
|
+
const task = store.get(id);
|
|
398
|
+
sendJson(res, 200, { ok: true, task });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (action === 'toggle') {
|
|
403
|
+
const current = store.get(id);
|
|
404
|
+
if (!current) {
|
|
405
|
+
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const task = scheduler.toggleTask(id);
|
|
409
|
+
sendJson(res, 200, { ok: true, task });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (action === 'delete') {
|
|
414
|
+
const current = store.get(id);
|
|
415
|
+
if (current) {
|
|
416
|
+
scheduler.pauseTask(id);
|
|
417
|
+
store.delete(id);
|
|
418
|
+
}
|
|
419
|
+
sendJson(res, 200, { ok: true });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
sendJson(res, 405, { ok: false, error: 'Action not supported' });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}), 'dsh-cron: /action/:id/:action');
|
|
429
|
+
|
|
317
430
|
// 2. Tools
|
|
318
431
|
ctx.tools.register(defineTool({
|
|
319
432
|
name: 'cron_create_task',
|
|
@@ -345,13 +458,73 @@ export function apply(ctx, config) {
|
|
|
345
458
|
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
346
459
|
},
|
|
347
460
|
execute: async (args) => {
|
|
461
|
+
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
462
|
+
const parsed = parseScheduleExpression(args.schedule);
|
|
463
|
+
const task = store.set({
|
|
464
|
+
title: args.title,
|
|
465
|
+
schedule: parsed.cronPattern || args.schedule,
|
|
466
|
+
scheduleText: parsed.humanText,
|
|
467
|
+
prompt: args.prompt,
|
|
468
|
+
type: taskType,
|
|
469
|
+
delivery: args.delivery || 'isolated',
|
|
470
|
+
status: 'active',
|
|
471
|
+
provider: args.provider,
|
|
472
|
+
model: args.model,
|
|
473
|
+
notifyTelegram: Boolean(args.notifyTelegram),
|
|
474
|
+
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
475
|
+
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
476
|
+
overlapPolicy: args.overlapPolicy || 'skip',
|
|
477
|
+
kanbanMode: args.kanbanMode || 'none',
|
|
478
|
+
oneShot: Boolean(parsed.isOneShot),
|
|
479
|
+
});
|
|
480
|
+
scheduler.scheduleTask(task);
|
|
481
|
+
return {
|
|
482
|
+
success: true,
|
|
483
|
+
message: `Задача "${task.title}" успешно запланирована (${task.scheduleText})`,
|
|
484
|
+
task
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}));
|
|
488
|
+
|
|
489
|
+
// Alias tool cron_schedule_task for compatibility with agent prompts (#70)
|
|
490
|
+
ctx.tools.register(defineTool({
|
|
491
|
+
name: 'cron_schedule_task',
|
|
492
|
+
description: 'Алиас для cron_create_task: создать новую запланированную задачу cron',
|
|
493
|
+
parameters: {
|
|
494
|
+
title: { type: 'string', description: 'Краткое название задачи', required: true },
|
|
495
|
+
schedule: { type: 'string', description: 'Расписание: cron, интервал или разово', required: true },
|
|
496
|
+
prompt: { type: 'string', description: 'Промпт/инструкция для агента при запуске', required: true },
|
|
497
|
+
type: { type: 'string', enum: ['llm', 'prompt', 'script'], description: 'Тип задачи' },
|
|
498
|
+
delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Режим запуска' },
|
|
499
|
+
provider: { type: 'string', description: 'Провайдер модели (опционально)' },
|
|
500
|
+
model: { type: 'string', description: 'Модель (опционально)' },
|
|
501
|
+
notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
|
|
502
|
+
onlyOnFailure: { type: 'boolean', description: 'Отправлять отчет только при сбоях' },
|
|
503
|
+
timeoutSeconds: { type: 'number', description: 'Лимит времени выполнения в секундах' },
|
|
504
|
+
overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Политика перекрытия' },
|
|
505
|
+
kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Создавать карточку в Kanban' },
|
|
506
|
+
},
|
|
507
|
+
output: {
|
|
508
|
+
schema: {
|
|
509
|
+
type: 'object',
|
|
510
|
+
additionalProperties: true,
|
|
511
|
+
properties: {
|
|
512
|
+
success: { type: 'boolean' },
|
|
513
|
+
message: { type: 'string' },
|
|
514
|
+
task: { type: 'object', additionalProperties: true }
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
518
|
+
},
|
|
519
|
+
execute: async (args) => {
|
|
520
|
+
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
348
521
|
const parsed = parseScheduleExpression(args.schedule);
|
|
349
522
|
const task = store.set({
|
|
350
523
|
title: args.title,
|
|
351
524
|
schedule: parsed.cronPattern || args.schedule,
|
|
352
525
|
scheduleText: parsed.humanText,
|
|
353
526
|
prompt: args.prompt,
|
|
354
|
-
type:
|
|
527
|
+
type: taskType,
|
|
355
528
|
delivery: args.delivery || 'isolated',
|
|
356
529
|
status: 'active',
|
|
357
530
|
provider: args.provider,
|
package/lib/prompt.js
CHANGED
|
@@ -11,7 +11,7 @@ export function buildAgentCronPrompt(userText) {
|
|
|
11
11
|
|
|
12
12
|
ВАЖНЕЙШИЕ ПРАВИЛА ДЛЯ АГЕНТА:
|
|
13
13
|
1. СТРОГИЙ ЗАПРЕТ НА СПЕШКУ:
|
|
14
|
-
КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вызывать инструмент \`cron_schedule_task\` в первом же ответе!
|
|
14
|
+
КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вызывать инструмент \`cron_create_task\` (или \`cron_schedule_task\`) в первом же ответе!
|
|
15
15
|
Ты НЕ имеешь права создавать задачу, пока не задашь пользователю уточняющие вопросы и не получишь его подтверждение.
|
|
16
16
|
|
|
17
17
|
2. ПЕРВЫЙ ВОПРОС: LLM vs NO-LLM Shell (КРИТИЧЕСКИ ВАЖНО):
|
|
@@ -44,7 +44,7 @@ export function buildAgentCronPrompt(userText) {
|
|
|
44
44
|
- Задай недостающие вопросы и попроси подтвердить создание задачи.
|
|
45
45
|
|
|
46
46
|
5. СОЗДАНИЕ ЗАДАЧИ:
|
|
47
|
-
Только ПОСЛЕ ТОГО, как пользователь ответит и подтвердит параметры, вызови инструмент \`
|
|
47
|
+
Только ПОСЛЕ ТОГО, как пользователь ответит и подтвердит параметры, вызови инструмент \`cron_create_task\`, передав:
|
|
48
48
|
- title: понятное название задачи
|
|
49
49
|
- schedule: согласованное cron-выражение
|
|
50
50
|
- prompt: финальная shell-команда/путь к скрипту (для type="script") ИЛИ подробная инструкция с Silent Rule (для type="llm")
|
package/lib/runner.js
CHANGED
|
@@ -82,14 +82,18 @@ export class SessionRunner {
|
|
|
82
82
|
}
|
|
83
83
|
} catch {}
|
|
84
84
|
|
|
85
|
-
let
|
|
85
|
+
let sessionModule = null;
|
|
86
86
|
try {
|
|
87
|
-
|
|
87
|
+
sessionModule = await import('@deepseek-ai/dsh-session');
|
|
88
|
+
} catch {}
|
|
89
|
+
|
|
90
|
+
const mintSessionId = () => {
|
|
91
|
+
const sid = `cron-exec-${task.id}-${randomUUID()}`;
|
|
88
92
|
if (sessionModule && typeof sessionModule.SessionId === 'function') {
|
|
89
|
-
|
|
90
|
-
mintSessionId = () => sessionModule.SessionId(sid);
|
|
93
|
+
return sessionModule.SessionId(sid);
|
|
91
94
|
}
|
|
92
|
-
|
|
95
|
+
return sid;
|
|
96
|
+
};
|
|
93
97
|
|
|
94
98
|
const defaultSel = this.ctx.get?.('agentDefaultModel')?.currentSelection?.();
|
|
95
99
|
const provider = task.provider || defaultSel?.provider;
|
|
@@ -112,6 +116,11 @@ export class SessionRunner {
|
|
|
112
116
|
source: { kind: 'plugin', plugin: 'dsh-cron', form: 'cron-execute' },
|
|
113
117
|
}));
|
|
114
118
|
|
|
119
|
+
// Await agent turn completion (#67)
|
|
120
|
+
if (typeof handle.agent.whenIdle === 'function') {
|
|
121
|
+
await handle.agent.whenIdle();
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
// Try extracting token usage from session history if available
|
|
116
125
|
let usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
|
|
117
126
|
try {
|
|
@@ -128,7 +137,7 @@ export class SessionRunner {
|
|
|
128
137
|
const costUsd = estimateTokenCost(model, usage);
|
|
129
138
|
|
|
130
139
|
return {
|
|
131
|
-
output: `[dsh-cron] Agent
|
|
140
|
+
output: `[dsh-cron] Agent finished turn for task "${task.title}" (session: ${handle.agent.session?.id || 'cron'})`,
|
|
132
141
|
usage,
|
|
133
142
|
costUsd
|
|
134
143
|
};
|
package/lib/scheduler.js
CHANGED
|
@@ -275,8 +275,12 @@ export class TaskScheduler {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
} catch (err) {
|
|
278
|
-
status = 'error';
|
|
279
278
|
error = err.message || String(err);
|
|
279
|
+
if (error.toLowerCase().includes('timed out') || error.toLowerCase().includes('timeout')) {
|
|
280
|
+
status = 'timeout';
|
|
281
|
+
} else {
|
|
282
|
+
status = 'error';
|
|
283
|
+
}
|
|
280
284
|
} finally {
|
|
281
285
|
const durationMs = Date.now() - start;
|
|
282
286
|
const runInfo = {
|
|
@@ -404,6 +408,16 @@ export class TaskScheduler {
|
|
|
404
408
|
return task;
|
|
405
409
|
}
|
|
406
410
|
|
|
411
|
+
toggleTask(taskId) {
|
|
412
|
+
const task = this.store.get(taskId);
|
|
413
|
+
if (!task) return null;
|
|
414
|
+
if (task.status === 'active') {
|
|
415
|
+
return this.pauseTask(taskId);
|
|
416
|
+
} else {
|
|
417
|
+
return this.resumeTask(taskId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
407
421
|
removeTask(taskId) {
|
|
408
422
|
if (this.jobs.has(taskId)) {
|
|
409
423
|
this.jobs.get(taskId).stop();
|
package/lib/store.js
CHANGED
|
@@ -77,10 +77,39 @@ export class TaskStore {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
getClientSettings() {
|
|
81
|
+
const raw = this.getSettings();
|
|
82
|
+
const isCustom = Boolean(this.settings.botToken);
|
|
83
|
+
const hasToken = Boolean(raw.botToken);
|
|
84
|
+
const masked = hasToken ? (raw.botToken.length > 8 ? raw.botToken.slice(0, 4) + '••••••••' + raw.botToken.slice(-3) : '••••••••') : '';
|
|
85
|
+
return {
|
|
86
|
+
botToken: masked,
|
|
87
|
+
botTokenMasked: masked,
|
|
88
|
+
hasBotToken: hasToken,
|
|
89
|
+
hasCustomBotToken: isCustom,
|
|
90
|
+
chatId: raw.chatId,
|
|
91
|
+
notifyTelegram: raw.notifyTelegram,
|
|
92
|
+
onlyOnFailure: raw.onlyOnFailure,
|
|
93
|
+
hasDefaultCredentials: raw.hasDefaultCredentials,
|
|
94
|
+
kanbanBaseUrl: raw.kanbanBaseUrl,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
saveSettings(newSettings = {}) {
|
|
99
|
+
let botTokenToSave = this.settings.botToken || '';
|
|
100
|
+
if (newSettings.botToken !== undefined) {
|
|
101
|
+
const val = String(newSettings.botToken).trim();
|
|
102
|
+
// If client sends masked token or empty string while custom token existed, don't overwrite with dots
|
|
103
|
+
if (val && !val.includes('••') && !val.includes('****')) {
|
|
104
|
+
botTokenToSave = val;
|
|
105
|
+
} else if (val === '') {
|
|
106
|
+
botTokenToSave = '';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
this.settings = {
|
|
82
111
|
...this.settings,
|
|
83
|
-
botToken:
|
|
112
|
+
botToken: botTokenToSave,
|
|
84
113
|
chatId: newSettings.chatId !== undefined ? String(newSettings.chatId).trim() : (this.settings.chatId || ''),
|
|
85
114
|
notifyTelegram: newSettings.notifyTelegram !== undefined ? Boolean(newSettings.notifyTelegram) : Boolean(this.settings.notifyTelegram),
|
|
86
115
|
onlyOnFailure: newSettings.onlyOnFailure !== undefined ? Boolean(newSettings.onlyOnFailure) : Boolean(this.settings.onlyOnFailure),
|