@goodandready/dsh-cron 0.2.47 → 0.2.49
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/api.js +1 -1
- package/lib/channels.js +15 -2
- package/lib/client.js +92 -47
- package/lib/config-jobs.js +2 -1
- package/lib/cron-tool.js +3 -2
- package/lib/http-utils.js +13 -5
- package/lib/index.js +11 -14
- package/lib/routes.js +1 -1
- package/lib/runtimes.js +0 -2
- package/lib/scheduler-execution.js +15 -14
- package/lib/scheduler.js +10 -8
- package/lib/settings.js +3 -1
- package/lib/store.js +8 -2
- package/lib/task-create.js +11 -3
- package/lib/task-patch.js +18 -8
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -547,7 +547,7 @@ export async function handleExternalTaskRequest({ store, scheduler, req, res, ur
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
if (req.method === 'POST') {
|
|
550
|
-
const handled = await handleItemPost({ store, scheduler, req, res, url, id, action
|
|
550
|
+
const handled = await handleItemPost({ store, scheduler, req, res, url, id, action });
|
|
551
551
|
if (handled) return;
|
|
552
552
|
}
|
|
553
553
|
if (req.method === 'GET' && !action) {
|
package/lib/channels.js
CHANGED
|
@@ -153,14 +153,27 @@ export function buildGiteaIssuePayload({ task, runInfo, settings = {} }) {
|
|
|
153
153
|
const failed = isFailed(runInfo.status);
|
|
154
154
|
const defaultNames = failed ? ['cron', 'bug', 'alert'] : ['cron', 'auto'];
|
|
155
155
|
|
|
156
|
-
// Resolve labels to integer IDs per Gitea Swagger (array<int64>) (#224)
|
|
157
|
-
|
|
156
|
+
// Resolve labels to integer IDs per Gitea Swagger (array<int64>) (#224, #297)
|
|
157
|
+
let mapping = settings.giteaLabelIds || {};
|
|
158
|
+
if (typeof mapping === 'string' && mapping.trim().startsWith('{')) {
|
|
159
|
+
try {
|
|
160
|
+
mapping = JSON.parse(mapping);
|
|
161
|
+
} catch (_) {
|
|
162
|
+
mapping = {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
158
165
|
let candidateIds = [];
|
|
159
166
|
|
|
160
167
|
if (Array.isArray(task && task.giteaLabelIds)) {
|
|
161
168
|
candidateIds = task.giteaLabelIds;
|
|
162
169
|
} else if (Array.isArray(settings.giteaLabels)) {
|
|
163
170
|
candidateIds = settings.giteaLabels;
|
|
171
|
+
} else if (typeof settings.giteaLabels === 'string' && settings.giteaLabels.trim()) {
|
|
172
|
+
candidateIds = settings.giteaLabels.split(',').map((s) => s.trim()).filter(Boolean).map((name) => {
|
|
173
|
+
const num = Number(name);
|
|
174
|
+
if (Number.isInteger(num) && num > 0) return num;
|
|
175
|
+
return typeof mapping === 'object' && mapping !== null ? mapping[name] : undefined;
|
|
176
|
+
});
|
|
164
177
|
} else if (Array.isArray(settings.giteaLabelIds)) {
|
|
165
178
|
candidateIds = settings.giteaLabelIds;
|
|
166
179
|
} else if (Array.isArray(task && task.labels)) {
|
package/lib/client.js
CHANGED
|
@@ -1102,6 +1102,54 @@ window.__ModuleLoader__.load({
|
|
|
1102
1102
|
return (typeof t === 'function' ? t : translate)(key, vars);
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* Issue #307: universal fetch with AbortController timeout (default 15s).
|
|
1107
|
+
* Connects with caller signal if supplied and fails closed on timeouts.
|
|
1108
|
+
*/
|
|
1109
|
+
const fetchWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1110
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1111
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1112
|
+
if (options && options.signal && controller) {
|
|
1113
|
+
if (options.signal.aborted) {
|
|
1114
|
+
controller.abort();
|
|
1115
|
+
} else {
|
|
1116
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const res = await fetch(url, {
|
|
1121
|
+
...options,
|
|
1122
|
+
signal: controller ? controller.signal : (options ? options.signal : undefined),
|
|
1123
|
+
});
|
|
1124
|
+
return res;
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
if (err && (err.name === 'AbortError' || controller?.signal?.aborted)) {
|
|
1127
|
+
const timeoutErr = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
1128
|
+
timeoutErr.isTimeout = true;
|
|
1129
|
+
throw timeoutErr;
|
|
1130
|
+
}
|
|
1131
|
+
throw err;
|
|
1132
|
+
} finally {
|
|
1133
|
+
if (timer) clearTimeout(timer);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Issue #307: universal JSON fetch helper with timeout and aborted body protection.
|
|
1139
|
+
*/
|
|
1140
|
+
const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1141
|
+
const res = await fetchWithTimeout(url, options, timeoutMs);
|
|
1142
|
+
let data = {};
|
|
1143
|
+
try {
|
|
1144
|
+
data = await res.json();
|
|
1145
|
+
} catch (jsonErr) {
|
|
1146
|
+
if (jsonErr && jsonErr.name === 'AbortError') {
|
|
1147
|
+
throw jsonErr;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return { res, data };
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1105
1153
|
function pickDeliverySettings(source) {
|
|
1106
1154
|
const out = {};
|
|
1107
1155
|
for (const key of DELIVERY_SETTING_KEYS.concat(TEMPLATE_SETTING_KEYS)) {
|
|
@@ -1391,8 +1439,12 @@ window.__ModuleLoader__.load({
|
|
|
1391
1439
|
const gen = ++fetchGenRef.current;
|
|
1392
1440
|
setLoading(true);
|
|
1393
1441
|
setFetchError(null);
|
|
1442
|
+
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1443
|
+
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
1394
1444
|
try {
|
|
1395
|
-
const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query)
|
|
1445
|
+
const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), {
|
|
1446
|
+
signal: controller ? controller.signal : undefined,
|
|
1447
|
+
});
|
|
1396
1448
|
const data = await res.json();
|
|
1397
1449
|
if (gen !== fetchGenRef.current) return;
|
|
1398
1450
|
if (data && data.ok) {
|
|
@@ -1407,6 +1459,7 @@ window.__ModuleLoader__.load({
|
|
|
1407
1459
|
console.error('[dsh-cron] fetch tasks error:', err);
|
|
1408
1460
|
setFetchError(err.message || T_KEY(t, 'list.loadErrorPrefix'));
|
|
1409
1461
|
} finally {
|
|
1462
|
+
if (timer) clearTimeout(timer);
|
|
1410
1463
|
if (gen === fetchGenRef.current) {
|
|
1411
1464
|
setLoading(false);
|
|
1412
1465
|
}
|
|
@@ -1414,9 +1467,11 @@ window.__ModuleLoader__.load({
|
|
|
1414
1467
|
};
|
|
1415
1468
|
|
|
1416
1469
|
const loadModels = async (providerName) => {
|
|
1470
|
+
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1471
|
+
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
1417
1472
|
try {
|
|
1418
1473
|
const url = providerName ? '/dsh-cron/models?provider=' + encodeURIComponent(providerName) : '/dsh-cron/models';
|
|
1419
|
-
const res = await fetch(url);
|
|
1474
|
+
const res = await fetch(url, { signal: controller ? controller.signal : undefined });
|
|
1420
1475
|
const data = await res.json();
|
|
1421
1476
|
if (data && data.ok) {
|
|
1422
1477
|
if (!providerName) {
|
|
@@ -1431,6 +1486,8 @@ window.__ModuleLoader__.load({
|
|
|
1431
1486
|
}
|
|
1432
1487
|
} catch (err) {
|
|
1433
1488
|
console.error('[dsh-cron] load models error:', err);
|
|
1489
|
+
} finally {
|
|
1490
|
+
if (timer) clearTimeout(timer);
|
|
1434
1491
|
}
|
|
1435
1492
|
};
|
|
1436
1493
|
|
|
@@ -1455,6 +1512,7 @@ window.__ModuleLoader__.load({
|
|
|
1455
1512
|
const fetchOpts = { headers };
|
|
1456
1513
|
if (pollController) fetchOpts.signal = pollController.signal;
|
|
1457
1514
|
|
|
1515
|
+
const pollTimer = pollController ? setTimeout(() => { try { pollController.abort(); } catch (abortErr) { void abortErr; } }, 15000) : null;
|
|
1458
1516
|
fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), fetchOpts)
|
|
1459
1517
|
.then(res => {
|
|
1460
1518
|
if (isDestroyed || pollGen !== fetchGenRef.current) return null;
|
|
@@ -1478,6 +1536,7 @@ window.__ModuleLoader__.load({
|
|
|
1478
1536
|
if (err && err.name === 'AbortError') return;
|
|
1479
1537
|
})
|
|
1480
1538
|
.finally(() => {
|
|
1539
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1481
1540
|
if (isDestroyed) return;
|
|
1482
1541
|
// Adaptive interval: 30s when tab is hidden, 8s when active (#134)
|
|
1483
1542
|
const intervalMs = (typeof document !== 'undefined' && document.hidden) ? 30000 : 8000;
|
|
@@ -1515,10 +1574,17 @@ window.__ModuleLoader__.load({
|
|
|
1515
1574
|
const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1516
1575
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1517
1576
|
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1577
|
+
if (options && options.signal && controller) {
|
|
1578
|
+
if (options.signal.aborted) {
|
|
1579
|
+
controller.abort();
|
|
1580
|
+
} else {
|
|
1581
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1518
1584
|
try {
|
|
1519
1585
|
const res = await fetch(url, {
|
|
1520
1586
|
...options,
|
|
1521
|
-
signal: controller ? controller.signal : options.signal,
|
|
1587
|
+
signal: controller ? controller.signal : (options ? options.signal : undefined),
|
|
1522
1588
|
});
|
|
1523
1589
|
let data = {};
|
|
1524
1590
|
try {
|
|
@@ -1589,8 +1655,7 @@ window.__ModuleLoader__.load({
|
|
|
1589
1655
|
/** #41: duplicate a task; the server copies config, resets run state (#41). */
|
|
1590
1656
|
const handleDryRun = async (taskId) => {
|
|
1591
1657
|
try {
|
|
1592
|
-
const res = await
|
|
1593
|
-
const data = await res.json();
|
|
1658
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/dry-run', { method: 'POST' });
|
|
1594
1659
|
setDryRunResult(data);
|
|
1595
1660
|
} catch (err) {
|
|
1596
1661
|
alert('Dry run error: ' + err.message);
|
|
@@ -1603,8 +1668,7 @@ window.__ModuleLoader__.load({
|
|
|
1603
1668
|
setArchiveLoading(true);
|
|
1604
1669
|
setArchiveModalOpen(true);
|
|
1605
1670
|
try {
|
|
1606
|
-
const res = await
|
|
1607
|
-
const data = await res.json();
|
|
1671
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/archive?limit=15&offset=' + offset);
|
|
1608
1672
|
if (data.ok) {
|
|
1609
1673
|
setArchiveRuns(prev => offset === 0 ? (data.runs || []) : [...prev, ...(data.runs || [])]);
|
|
1610
1674
|
setArchiveTotal(data.total || 0);
|
|
@@ -1618,12 +1682,11 @@ window.__ModuleLoader__.load({
|
|
|
1618
1682
|
|
|
1619
1683
|
const handlePreviewSchedule = async (scheduleExpr) => {
|
|
1620
1684
|
try {
|
|
1621
|
-
const res = await
|
|
1685
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/schedule/preview', {
|
|
1622
1686
|
method: 'POST',
|
|
1623
1687
|
headers: { 'Content-Type': 'application/json' },
|
|
1624
1688
|
body: JSON.stringify({ schedule: scheduleExpr, timezone: formTimezone })
|
|
1625
1689
|
});
|
|
1626
|
-
const data = await res.json();
|
|
1627
1690
|
if (data.ok && data.runs) {
|
|
1628
1691
|
setSchedulePreviewRuns(data.runs);
|
|
1629
1692
|
} else {
|
|
@@ -1636,8 +1699,7 @@ window.__ModuleLoader__.load({
|
|
|
1636
1699
|
|
|
1637
1700
|
const handleDuplicate = async (task) => {
|
|
1638
1701
|
try {
|
|
1639
|
-
const res = await
|
|
1640
|
-
const data = await res.json();
|
|
1702
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/duplicate', { method: 'POST' });
|
|
1641
1703
|
if (!data.ok) {
|
|
1642
1704
|
alert(T_KEY(t, 'duplicate.failed', { error: (data && data.error) || '' }));
|
|
1643
1705
|
return;
|
|
@@ -1678,8 +1740,7 @@ window.__ModuleLoader__.load({
|
|
|
1678
1740
|
const promptMsg = T_KEY(t, 'transfer.exportPrompt') || 'Choose export mode:\nOK = Sanitized (removes custom env vars & headers)\nCancel = Full raw export';
|
|
1679
1741
|
const wantSanitized = confirm(promptMsg);
|
|
1680
1742
|
const exportUrl = '/dsh-cron/tasks/export' + (wantSanitized ? '?sanitize=true' : '');
|
|
1681
|
-
const res = await
|
|
1682
|
-
const data = await res.json();
|
|
1743
|
+
const { res, data } = await fetchJsonWithTimeout(exportUrl);
|
|
1683
1744
|
if (!data.ok) {
|
|
1684
1745
|
alert(T_KEY(t, 'transfer.exportFailed', { error: (data && data.error) || '' }));
|
|
1685
1746
|
return;
|
|
@@ -1717,12 +1778,11 @@ window.__ModuleLoader__.load({
|
|
|
1717
1778
|
alert(T_KEY(t, 'transfer.badFile', { error: parseErr.message }));
|
|
1718
1779
|
return;
|
|
1719
1780
|
}
|
|
1720
|
-
const res = await
|
|
1781
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
|
|
1721
1782
|
method: 'POST',
|
|
1722
1783
|
headers: importHeaders(document_),
|
|
1723
1784
|
body: JSON.stringify({ document: document_, dryRun: true, strategy: importStrategy })
|
|
1724
1785
|
});
|
|
1725
|
-
const data = await res.json();
|
|
1726
1786
|
if (!data.ok) {
|
|
1727
1787
|
alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
|
|
1728
1788
|
return;
|
|
@@ -1740,12 +1800,11 @@ window.__ModuleLoader__.load({
|
|
|
1740
1800
|
if (!importDoc) return;
|
|
1741
1801
|
setTransferBusy(true);
|
|
1742
1802
|
try {
|
|
1743
|
-
const res = await
|
|
1803
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
|
|
1744
1804
|
method: 'POST',
|
|
1745
1805
|
headers: importHeaders(importDoc),
|
|
1746
1806
|
body: JSON.stringify({ document: importDoc, strategy: importStrategy })
|
|
1747
1807
|
});
|
|
1748
|
-
const data = await res.json();
|
|
1749
1808
|
if (!data.ok) {
|
|
1750
1809
|
alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
|
|
1751
1810
|
return;
|
|
@@ -1777,12 +1836,11 @@ window.__ModuleLoader__.load({
|
|
|
1777
1836
|
'If the change makes the task execute code (script/node/python/ssh/docker), confirm it with me explicitly before applying it.',
|
|
1778
1837
|
].filter(Boolean);
|
|
1779
1838
|
try {
|
|
1780
|
-
const res = await
|
|
1839
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
|
|
1781
1840
|
method: 'POST',
|
|
1782
1841
|
headers: { 'Content-Type': 'application/json' },
|
|
1783
1842
|
body: JSON.stringify({ prompt: lines.join(String.fromCharCode(10)) })
|
|
1784
1843
|
});
|
|
1785
|
-
const data = await res.json();
|
|
1786
1844
|
if (!data.ok || !data.sessionId) {
|
|
1787
1845
|
alert(T_KEY(t, 'tune.startFailed', { error: (data && data.error) || '' }));
|
|
1788
1846
|
return;
|
|
@@ -1798,7 +1856,7 @@ window.__ModuleLoader__.load({
|
|
|
1798
1856
|
const handleDelete = async (id) => {
|
|
1799
1857
|
if (!confirm(T_KEY(t, 'confirm.delete'))) return;
|
|
1800
1858
|
try {
|
|
1801
|
-
await
|
|
1859
|
+
await fetchWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
1802
1860
|
fetchTasks();
|
|
1803
1861
|
} catch (err) {
|
|
1804
1862
|
console.debug('[dsh-cron] delete failed', err);
|
|
@@ -1809,8 +1867,7 @@ window.__ModuleLoader__.load({
|
|
|
1809
1867
|
setTestStatus(null);
|
|
1810
1868
|
setSettingsModalOpen(true);
|
|
1811
1869
|
try {
|
|
1812
|
-
const res = await
|
|
1813
|
-
const data = await res.json();
|
|
1870
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings');
|
|
1814
1871
|
if (data && data.ok && data.settings) {
|
|
1815
1872
|
setSettingsBotToken(data.settings.botToken || '');
|
|
1816
1873
|
setSettingsChatId(data.settings.chatId || '');
|
|
@@ -1829,7 +1886,7 @@ window.__ModuleLoader__.load({
|
|
|
1829
1886
|
const handleSaveSettings = async (e) => {
|
|
1830
1887
|
e.preventDefault();
|
|
1831
1888
|
try {
|
|
1832
|
-
const res = await
|
|
1889
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
|
|
1833
1890
|
method: 'POST',
|
|
1834
1891
|
headers: { 'Content-Type': 'application/json' },
|
|
1835
1892
|
body: JSON.stringify(Object.assign({
|
|
@@ -1840,7 +1897,6 @@ window.__ModuleLoader__.load({
|
|
|
1840
1897
|
kanbanBaseUrl: settingsKanbanUrl,
|
|
1841
1898
|
}, settingsDelivery))
|
|
1842
1899
|
});
|
|
1843
|
-
const data = await res.json();
|
|
1844
1900
|
if (data && data.ok) {
|
|
1845
1901
|
setSettingsModalOpen(false);
|
|
1846
1902
|
} else {
|
|
@@ -1881,7 +1937,7 @@ window.__ModuleLoader__.load({
|
|
|
1881
1937
|
|
|
1882
1938
|
const handleTestTelegram = async () => { setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
1883
1939
|
try {
|
|
1884
|
-
const res = await
|
|
1940
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
|
|
1885
1941
|
method: 'POST',
|
|
1886
1942
|
headers: { 'Content-Type': 'application/json' },
|
|
1887
1943
|
body: JSON.stringify({
|
|
@@ -1889,7 +1945,6 @@ window.__ModuleLoader__.load({
|
|
|
1889
1945
|
chatId: settingsChatId
|
|
1890
1946
|
})
|
|
1891
1947
|
});
|
|
1892
|
-
const data = await res.json();
|
|
1893
1948
|
if (data && data.ok) {
|
|
1894
1949
|
setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
|
|
1895
1950
|
} else {
|
|
@@ -1969,8 +2024,7 @@ window.__ModuleLoader__.load({
|
|
|
1969
2024
|
const loadTaskHistory = async (taskId) => {
|
|
1970
2025
|
setHistoryLoading(true);
|
|
1971
2026
|
try {
|
|
1972
|
-
const res = await
|
|
1973
|
-
const data = await res.json();
|
|
2027
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/history');
|
|
1974
2028
|
if (data.ok) {
|
|
1975
2029
|
setTaskHistory(data.history || []);
|
|
1976
2030
|
}
|
|
@@ -2119,12 +2173,11 @@ window.__ModuleLoader__.load({
|
|
|
2119
2173
|
Boolean(payload.preflightCommand && payload.preflightCommand.trim()) ||
|
|
2120
2174
|
Boolean(payload.selfHealingCommand && payload.selfHealingCommand.trim());
|
|
2121
2175
|
if (hasCodeExec) headers[SCRIPT_CONFIRM_HEADER] = 'script';
|
|
2122
|
-
const res = await
|
|
2176
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks', {
|
|
2123
2177
|
method: 'POST',
|
|
2124
2178
|
headers,
|
|
2125
2179
|
body: JSON.stringify(payload)
|
|
2126
2180
|
});
|
|
2127
|
-
const data = await res.json();
|
|
2128
2181
|
if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
|
|
2129
2182
|
setManualModalOpen(false);
|
|
2130
2183
|
fetchTasks();
|
|
@@ -2143,12 +2196,11 @@ window.__ModuleLoader__.load({
|
|
|
2143
2196
|
|
|
2144
2197
|
setDshLoading(true);
|
|
2145
2198
|
try {
|
|
2146
|
-
const res = await
|
|
2199
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
|
|
2147
2200
|
method: 'POST',
|
|
2148
2201
|
headers: { 'Content-Type': 'application/json' },
|
|
2149
2202
|
body: JSON.stringify({ prompt: text })
|
|
2150
2203
|
});
|
|
2151
|
-
const data = await res.json();
|
|
2152
2204
|
if (!data.ok || !data.sessionId) {
|
|
2153
2205
|
alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
|
|
2154
2206
|
setDshLoading(false);
|
|
@@ -3532,8 +3584,7 @@ window.__ModuleLoader__.load({
|
|
|
3532
3584
|
const checkUpdates = async () => {
|
|
3533
3585
|
setUpdateState(prev => ({ ...prev, checking: true, error: null, notice: null }));
|
|
3534
3586
|
try {
|
|
3535
|
-
const res = await
|
|
3536
|
-
const data = await res.json();
|
|
3587
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update');
|
|
3537
3588
|
if (data && data.currentVersion) {
|
|
3538
3589
|
setUpdateState(prev => ({
|
|
3539
3590
|
...prev,
|
|
@@ -3554,11 +3605,10 @@ window.__ModuleLoader__.load({
|
|
|
3554
3605
|
const runUpdate = async () => {
|
|
3555
3606
|
setUpdateState(prev => ({ ...prev, updating: true, error: null, notice: null }));
|
|
3556
3607
|
try {
|
|
3557
|
-
const res = await
|
|
3608
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update', {
|
|
3558
3609
|
method: 'POST',
|
|
3559
3610
|
headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' }
|
|
3560
3611
|
});
|
|
3561
|
-
const data = await res.json();
|
|
3562
3612
|
if (data && data.updated) {
|
|
3563
3613
|
setUpdateState(prev => ({
|
|
3564
3614
|
...prev,
|
|
@@ -3587,9 +3637,8 @@ window.__ModuleLoader__.load({
|
|
|
3587
3637
|
React.useEffect(() => {
|
|
3588
3638
|
let active = true;
|
|
3589
3639
|
setLoadState('loading');
|
|
3590
|
-
|
|
3591
|
-
.then(
|
|
3592
|
-
.then(data => {
|
|
3640
|
+
fetchJsonWithTimeout('/dsh-cron/settings')
|
|
3641
|
+
.then(({ data }) => {
|
|
3593
3642
|
if (!active) return;
|
|
3594
3643
|
if (data && data.ok && data.settings) {
|
|
3595
3644
|
setToken(data.settings.botToken || '');
|
|
@@ -3615,7 +3664,7 @@ window.__ModuleLoader__.load({
|
|
|
3615
3664
|
const saveSettings = async (e) => {
|
|
3616
3665
|
if (e) e.preventDefault();
|
|
3617
3666
|
try {
|
|
3618
|
-
const res = await
|
|
3667
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
|
|
3619
3668
|
method: 'POST',
|
|
3620
3669
|
headers: { 'Content-Type': 'application/json' },
|
|
3621
3670
|
body: JSON.stringify(Object.assign({
|
|
@@ -3627,7 +3676,6 @@ window.__ModuleLoader__.load({
|
|
|
3627
3676
|
kanbanBaseUrl: kanbanUrl
|
|
3628
3677
|
}, delivery))
|
|
3629
3678
|
});
|
|
3630
|
-
const data = await res.json();
|
|
3631
3679
|
if (data && data.ok) {
|
|
3632
3680
|
setSaved(true);
|
|
3633
3681
|
setTimeout(() => setSaved(false), 2500);
|
|
@@ -3640,12 +3688,11 @@ window.__ModuleLoader__.load({
|
|
|
3640
3688
|
const testTelegram = async () => {
|
|
3641
3689
|
setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
3642
3690
|
try {
|
|
3643
|
-
const res = await
|
|
3691
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
|
|
3644
3692
|
method: 'POST',
|
|
3645
3693
|
headers: { 'Content-Type': 'application/json' },
|
|
3646
3694
|
body: JSON.stringify({ botToken: token, chatId })
|
|
3647
3695
|
});
|
|
3648
|
-
const data = await res.json();
|
|
3649
3696
|
if (data && data.ok) {
|
|
3650
3697
|
setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
|
|
3651
3698
|
} else {
|
|
@@ -3659,12 +3706,11 @@ window.__ModuleLoader__.load({
|
|
|
3659
3706
|
const testKanban = async () => {
|
|
3660
3707
|
setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
|
|
3661
3708
|
try {
|
|
3662
|
-
const res = await
|
|
3709
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/kanban/test', {
|
|
3663
3710
|
method: 'POST',
|
|
3664
3711
|
headers: { 'Content-Type': 'application/json' },
|
|
3665
3712
|
body: JSON.stringify({ kanbanBaseUrl: kanbanUrl })
|
|
3666
3713
|
});
|
|
3667
|
-
const data = await res.json();
|
|
3668
3714
|
if (data && data.ok) {
|
|
3669
3715
|
setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
|
|
3670
3716
|
} else {
|
|
@@ -3971,8 +4017,7 @@ window.__ModuleLoader__.load({
|
|
|
3971
4017
|
|
|
3972
4018
|
async function loadSidebarJobs(state) {
|
|
3973
4019
|
try {
|
|
3974
|
-
const res = await
|
|
3975
|
-
const data = await res.json();
|
|
4020
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks?status=active');
|
|
3976
4021
|
if (data && data.ok) renderSidebarJobs(state, data.tasks || []);
|
|
3977
4022
|
} catch (err) {
|
|
3978
4023
|
console.debug('[dsh-cron] load sidebar jobs failed', err);
|
package/lib/config-jobs.js
CHANGED
|
@@ -17,6 +17,7 @@ import { parseScheduleExpression } from './scheduler.js';
|
|
|
17
17
|
import { unknownChannelIds } from './channels.js';
|
|
18
18
|
import { PATCHABLE_TASK_FIELDS, validateTaskType } from './task-transfer.js';
|
|
19
19
|
import { pickPatchableFields } from './http-utils.js';
|
|
20
|
+
import { logger } from './logger.js';
|
|
20
21
|
|
|
21
22
|
/** Marker that makes a task owned by the config (#50). */
|
|
22
23
|
export const MANAGED_BY_CONFIG = 'config';
|
|
@@ -171,7 +172,7 @@ export function planConfigSync(entries, existingTasks) {
|
|
|
171
172
|
* Apply the plan: create, update and remove tasks, then arm or pause them.
|
|
172
173
|
* Never throws for a single bad entry — that entry is reported and skipped.
|
|
173
174
|
*/
|
|
174
|
-
export function applyConfigSync({ store, scheduler, entries, log =
|
|
175
|
+
export function applyConfigSync({ store, scheduler, entries, log = logger }) {
|
|
175
176
|
const plan = planConfigSync(entries, store.list({ status: 'all' }));
|
|
176
177
|
for (const warning of plan.warnings) log.warn('[dsh-cron] config jobs: ' + warning);
|
|
177
178
|
for (const entry of plan.skipped) log.error('[dsh-cron] config jobs: skipped: ' + entry.error);
|
package/lib/cron-tool.js
CHANGED
|
@@ -3,6 +3,7 @@ import { applyTaskPatch, describeTaskPatch } from './task-patch.js';
|
|
|
3
3
|
import { isConfigOwned, configOwnedMessage } from './config-jobs.js';
|
|
4
4
|
import { TASK_TYPES } from './runtimes.js';
|
|
5
5
|
import { CHANNEL_IDS } from './channels.js';
|
|
6
|
+
import { redactTaskSecrets } from './http-utils.js';
|
|
6
7
|
|
|
7
8
|
export const LEGACY_TOOL_NAMES = [
|
|
8
9
|
'cron_create_task',
|
|
@@ -394,7 +395,7 @@ export async function executeCronTool(store, scheduler, args = {}) {
|
|
|
394
395
|
`channels: ${Array.isArray(task.channels) && task.channels.length ? task.channels.join(', ') : '(legacy flags)'}`,
|
|
395
396
|
`prompt: ${String(task.prompt || '').slice(0, 500)}`,
|
|
396
397
|
].filter(Boolean).join('\n');
|
|
397
|
-
return { success: true, message: summary, task };
|
|
398
|
+
return { success: true, message: summary, task: redactTaskSecrets(task) };
|
|
398
399
|
}
|
|
399
400
|
|
|
400
401
|
case 'update': {
|
|
@@ -411,7 +412,7 @@ export async function executeCronTool(store, scheduler, args = {}) {
|
|
|
411
412
|
return {
|
|
412
413
|
success: true,
|
|
413
414
|
message: `Task "${result.task.title}" updated (${result.task.scheduleText || result.task.schedule}). Changed: ${describeTaskPatch(result.task, result.patch)}`,
|
|
414
|
-
task: result.task,
|
|
415
|
+
task: redactTaskSecrets(result.task),
|
|
415
416
|
};
|
|
416
417
|
}
|
|
417
418
|
|
package/lib/http-utils.js
CHANGED
|
@@ -94,15 +94,17 @@ export function isTrustedRequest(req, options = {}) {
|
|
|
94
94
|
|
|
95
95
|
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress;
|
|
96
96
|
const hasRemote = Boolean(remote && typeof remote === 'string');
|
|
97
|
-
const isLoopback =
|
|
97
|
+
const isLoopback = Boolean(hasRemote && isLoopbackAddress(remote));
|
|
98
98
|
|
|
99
99
|
const token = extractBearerToken(req);
|
|
100
100
|
const expectedToken = options.apiToken || process.env.DSH_AUTH_TOKEN || process.env.DSH_TOKEN;
|
|
101
101
|
const isTokenAuthenticated = Boolean(expectedToken && token && token === expectedToken);
|
|
102
102
|
|
|
103
|
-
// 1. Non-loopback
|
|
104
|
-
if (
|
|
105
|
-
|
|
103
|
+
// 1. Non-loopback socket callers (or callers with missing remoteAddress) require explicit token authentication (#86, #295)
|
|
104
|
+
if (req.socket || req.connection || hasRemote) {
|
|
105
|
+
if (!isLoopback && !isTokenAuthenticated) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// 2. Validate Origin header if present: cannot be null, must match Host
|
|
@@ -178,7 +180,13 @@ export async function readBody(req, res) {
|
|
|
178
180
|
*/
|
|
179
181
|
export function redactTaskSecrets(task) {
|
|
180
182
|
if (!task || typeof task !== 'object') return task;
|
|
181
|
-
|
|
183
|
+
// Issue #310 / GH #5: sanitize to lossless JSON by omitting any undefined properties
|
|
184
|
+
const clone = {};
|
|
185
|
+
for (const [k, v] of Object.entries(task)) {
|
|
186
|
+
if (v !== undefined) {
|
|
187
|
+
clone[k] = v;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
182
190
|
|
|
183
191
|
if (clone.env && typeof clone.env === 'object') {
|
|
184
192
|
const redactedEnv = {};
|
package/lib/index.js
CHANGED
|
@@ -117,22 +117,20 @@ export {
|
|
|
117
117
|
LEGACY_ACTION_MAP,
|
|
118
118
|
};
|
|
119
119
|
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
120
|
-
// Helper to access configuration safely in native Cordis Context without throwing missing-inject error (#235)
|
|
121
|
-
function getContextConfig(ctx, rawConfig) {
|
|
120
|
+
// Helper to access configuration safely in native Cordis Context without throwing missing-inject error (#235, #288)
|
|
121
|
+
export function getContextConfig(ctx, rawConfig) {
|
|
122
122
|
try {
|
|
123
123
|
if (ctx && ctx.fiber && ctx.fiber.config) return ctx.fiber.config;
|
|
124
124
|
if (ctx && ctx.scope && ctx.scope.config) return ctx.scope.config;
|
|
125
125
|
if (ctx && ctx.runtime && ctx.runtime.config) return ctx.runtime.config;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Ignore missing inject error on native Cordis Context (#235)
|
|
132
|
-
}
|
|
126
|
+
// In plain/mock contexts without fiber/scope, safely attempt reading config
|
|
127
|
+
try {
|
|
128
|
+
if (ctx && ctx.config) return ctx.config;
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// Ignored if ctx is a Cordis proxy throwing without inject (#235, #288)
|
|
133
131
|
}
|
|
134
132
|
} catch (err) {
|
|
135
|
-
// Ignore context access error and fall back to rawConfig (#235)
|
|
133
|
+
// Ignore context access error and fall back to rawConfig (#235, #288)
|
|
136
134
|
}
|
|
137
135
|
return rawConfig || {};
|
|
138
136
|
}
|
|
@@ -199,7 +197,7 @@ export function apply(ctx, rawConfig) {
|
|
|
199
197
|
// A broken entry is reported and skipped, never fatal.
|
|
200
198
|
try {
|
|
201
199
|
const configJobs = Array.isArray(config && config.jobs) ? config.jobs : [];
|
|
202
|
-
const summary = applyConfigSync({ store, scheduler, entries: configJobs });
|
|
200
|
+
const summary = applyConfigSync({ store, scheduler, entries: configJobs, log: logger });
|
|
203
201
|
logger.info('[dsh-cron] config jobs synced: ' + JSON.stringify(summary));
|
|
204
202
|
} catch (err) {
|
|
205
203
|
logger.error('[dsh-cron] config job sync failed:', err.message);
|
|
@@ -253,7 +251,7 @@ export function apply(ctx, rawConfig) {
|
|
|
253
251
|
}
|
|
254
252
|
const current = store.getSettings();
|
|
255
253
|
const patch = {};
|
|
256
|
-
for (const k of
|
|
254
|
+
for (const k of SETTINGS_SYNC_KEYS) {
|
|
257
255
|
if (unwrapped[k] !== undefined && unwrapped[k] !== current[k]) {
|
|
258
256
|
patch[k] = unwrapped[k];
|
|
259
257
|
}
|
|
@@ -295,7 +293,6 @@ export function apply(ctx, rawConfig) {
|
|
|
295
293
|
registerRoutes(ctx, {
|
|
296
294
|
store,
|
|
297
295
|
scheduler,
|
|
298
|
-
getCronSettingsScope: () => settingsService,
|
|
299
296
|
recommendations,
|
|
300
297
|
});
|
|
301
298
|
|
|
@@ -311,7 +308,7 @@ export function apply(ctx, rawConfig) {
|
|
|
311
308
|
...activeVolatileConfig,
|
|
312
309
|
};
|
|
313
310
|
const clientSettings = store.getClientSettings();
|
|
314
|
-
for (const k of
|
|
311
|
+
for (const k of SETTINGS_SYNC_KEYS) {
|
|
315
312
|
if (unwrapped[k] !== undefined) {
|
|
316
313
|
clientSettings[k] = unwrapped[k];
|
|
317
314
|
}
|
package/lib/routes.js
CHANGED
|
@@ -10,7 +10,7 @@ import { sendTelegramMessage, createTelegramTaskKeyboard } from './telegram.js';
|
|
|
10
10
|
import { createKanbanCard } from './integrations.js';
|
|
11
11
|
import { sanitizeSettingsPayload, applySettingsToScope } from './settings.js';
|
|
12
12
|
|
|
13
|
-
export function registerRoutes(ctx, { store, scheduler,
|
|
13
|
+
export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
14
14
|
// handler per route family; registered WITHOUT the trailing slash (#96)
|
|
15
15
|
ctx.effect(() => ctx.webServer.register({
|
|
16
16
|
kind: 'prefix',
|
package/lib/runtimes.js
CHANGED
|
@@ -124,8 +124,6 @@ export function buildSshInvocation(task) {
|
|
|
124
124
|
return { file: 'ssh', args };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export const DOCKER_HOST_ENV_ALLOWLIST = ['TZ', 'LANG', 'LC_ALL'];
|
|
128
|
-
|
|
129
127
|
/** #8/#226: build Docker run invocation without leaking host process secrets into container. */
|
|
130
128
|
export function buildDockerInvocation(task, explicitEnv) {
|
|
131
129
|
const image = String(task.dockerImage || '').trim();
|
|
@@ -40,15 +40,17 @@ export async function executePreflight(task) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (type === 'http') {
|
|
43
|
+
let timeout;
|
|
43
44
|
try {
|
|
44
45
|
const controller = new AbortController();
|
|
45
|
-
|
|
46
|
+
timeout = setTimeout(() => controller.abort(), 5000);
|
|
46
47
|
const res = await fetch(target, { signal: controller.signal });
|
|
47
|
-
clearTimeout(timeout);
|
|
48
48
|
if (res.ok) return { ok: true };
|
|
49
49
|
return { ok: false, reason: `HTTP check returned status ${res.status}` };
|
|
50
50
|
} catch (err) {
|
|
51
51
|
return { ok: false, reason: `HTTP check failed: ${err.message}` };
|
|
52
|
+
} finally {
|
|
53
|
+
if (timeout) clearTimeout(timeout);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -595,7 +597,7 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
595
597
|
const guard = await checkAndApplyBurnGuard(scheduler, taskId, outcome);
|
|
596
598
|
if (guard && guard.exceeded) return;
|
|
597
599
|
|
|
598
|
-
|
|
600
|
+
if (!scheduler.isStopped && scheduler.queue.length > 0 && (scheduler.maxConcurrent === 0 || scheduler.running.size < scheduler.maxConcurrent)) {
|
|
599
601
|
let admittedIndex = -1;
|
|
600
602
|
for (let i = 0; i < scheduler.queue.length; i++) {
|
|
601
603
|
const candidate = scheduler.queue[i];
|
|
@@ -619,17 +621,16 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
619
621
|
break;
|
|
620
622
|
}
|
|
621
623
|
|
|
622
|
-
if (admittedIndex
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
break;
|
|
624
|
+
if (admittedIndex !== -1) {
|
|
625
|
+
const nextItem = scheduler.queue.splice(admittedIndex, 1)[0];
|
|
626
|
+
setImmediate(() => {
|
|
627
|
+
if (!scheduler.isStopped) {
|
|
628
|
+
scheduler.runTask(nextItem.taskId, nextItem.options || {}).catch((runErr) => {
|
|
629
|
+
bestEffort('drain-queued-task', () => {}, scheduler.logger);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
633
634
|
}
|
|
634
635
|
|
|
635
636
|
// 1. Structured LLM Actions (#137)
|
package/lib/scheduler.js
CHANGED
|
@@ -140,15 +140,17 @@ function parseIntervalExpression(str) {
|
|
|
140
140
|
if (num >= 60) {
|
|
141
141
|
if (num % 60 === 0) {
|
|
142
142
|
const hours = num / 60;
|
|
143
|
-
if (hours >= 24
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
if (hours >= 24) {
|
|
144
|
+
if (hours % 24 === 0) {
|
|
145
|
+
const days = hours / 24;
|
|
146
|
+
if (days > 31) {
|
|
147
|
+
throw new Error(`Invalid interval: minute interval "${num}m" (${days} days) exceeds 31 days`);
|
|
148
|
+
}
|
|
149
|
+
pattern = `0 0 */${days} * *`;
|
|
150
|
+
humanText = `Every ${days} days`;
|
|
151
|
+
} else {
|
|
152
|
+
throw new Error(`Invalid interval: minute interval "${num}m" (${hours} hours) cannot be expressed as a cron step`);
|
|
147
153
|
}
|
|
148
|
-
pattern = `0 0 */${days} * *`;
|
|
149
|
-
humanText = `Every ${days} days`;
|
|
150
|
-
} else if (hours > 23) {
|
|
151
|
-
throw new Error(`Invalid interval: minute interval "${num}m" (${hours} hours) cannot be expressed as a cron step`);
|
|
152
154
|
} else {
|
|
153
155
|
pattern = `0 */${hours} * * *`;
|
|
154
156
|
humanText = `Every ${hours} hours`;
|
package/lib/settings.js
CHANGED
|
@@ -44,6 +44,8 @@ export const Config = z.object({
|
|
|
44
44
|
giteaBaseUrl: z.string().default('').description('Gitea base URL for alert issues').volatile(),
|
|
45
45
|
giteaRepo: z.string().default('').description('Gitea repository (owner/name) for alert issues').volatile(),
|
|
46
46
|
giteaTokenRef: z.string().default('').description('Credential name for the Gitea API token').volatile(),
|
|
47
|
+
giteaLabels: z.any().default([]).description('Gitea label names or IDs for alert issues').volatile(),
|
|
48
|
+
giteaLabelIds: z.any().default({}).description('Mapping of label name to integer ID for Gitea issues').volatile(),
|
|
47
49
|
prReviewerEnabled: z.boolean().default(false).description('Enable automated PR Reviewer scheduled agent (#33)').volatile(),
|
|
48
50
|
// --- Declarative jobs (#50, ADR-0001) ---
|
|
49
51
|
jobs: z.array(z.any()).default([]).description('Static jobs owned by the config; each entry needs id, title, schedule and (for agent types) prompt'),
|
|
@@ -79,7 +81,7 @@ export const SETTINGS_SYNC_KEYS = [
|
|
|
79
81
|
'pushplusTokenRef',
|
|
80
82
|
'pushplusUrl',
|
|
81
83
|
'ttsBaseUrl',
|
|
82
|
-
'giteaBaseUrl', 'giteaRepo', 'giteaTokenRef', 'prReviewerEnabled',
|
|
84
|
+
'giteaBaseUrl', 'giteaRepo', 'giteaTokenRef', 'giteaLabels', 'giteaLabelIds', 'prReviewerEnabled',
|
|
83
85
|
'apiToken',
|
|
84
86
|
];
|
|
85
87
|
|
package/lib/store.js
CHANGED
|
@@ -555,8 +555,8 @@ export class TaskStore {
|
|
|
555
555
|
type: task.type !== undefined ? task.type : (prev.type || 'llm'),
|
|
556
556
|
status: task.status !== undefined ? task.status : (prev.status || 'active'),
|
|
557
557
|
delivery: task.delivery !== undefined ? task.delivery : (prev.delivery || 'isolated'),
|
|
558
|
-
provider: task.provider !== undefined ? task.provider : prev.provider,
|
|
559
|
-
model: task.model !== undefined ? task.model : prev.model,
|
|
558
|
+
provider: task.provider !== undefined ? task.provider : (prev.provider || null),
|
|
559
|
+
model: task.model !== undefined ? task.model : (prev.model || null),
|
|
560
560
|
notifyTelegram: task.notifyTelegram !== undefined
|
|
561
561
|
? (task.notifyTelegram === null || task.notifyTelegram === 'inherit' ? undefined : Boolean(task.notifyTelegram))
|
|
562
562
|
: prev.notifyTelegram,
|
|
@@ -591,6 +591,12 @@ export class TaskStore {
|
|
|
591
591
|
lastDurationMs: task.lastDurationMs !== undefined ? task.lastDurationMs : (prev.lastDurationMs || 0),
|
|
592
592
|
nextRunAt: task.nextRunAt !== undefined ? task.nextRunAt : (prev.nextRunAt || null),
|
|
593
593
|
};
|
|
594
|
+
// Issue #310 / GH #5: strip any undefined properties to guarantee lossless JSON
|
|
595
|
+
for (const key of Object.keys(record)) {
|
|
596
|
+
if (record[key] === undefined) {
|
|
597
|
+
delete record[key];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
594
600
|
this.tasks.set(id, record);
|
|
595
601
|
try {
|
|
596
602
|
this.save();
|
package/lib/task-create.js
CHANGED
|
@@ -2,13 +2,14 @@ import { normalizeTaskType } from './runtimes.js';
|
|
|
2
2
|
import { validateTaskType } from './task-transfer.js';
|
|
3
3
|
import { parseScheduleExpression } from './scheduler.js';
|
|
4
4
|
import { CHANNEL_IDS } from './channels.js';
|
|
5
|
+
import { redactTaskSecrets } from './http-utils.js';
|
|
5
6
|
|
|
6
7
|
export function executeCreateTask(store, scheduler, args) {
|
|
7
8
|
const taskType = normalizeTaskType(args.type);
|
|
8
9
|
const typeError = validateTaskType(taskType, args);
|
|
9
10
|
if (typeError) throw new Error(typeError);
|
|
10
11
|
const parsed = parseScheduleExpression(args.schedule);
|
|
11
|
-
const
|
|
12
|
+
const taskData = {
|
|
12
13
|
title: args.title,
|
|
13
14
|
schedule: parsed.cronPattern || args.schedule,
|
|
14
15
|
scheduleText: parsed.humanText,
|
|
@@ -71,12 +72,19 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
71
72
|
costLimitUsd: Number(args.costLimitUsd) > 0 ? Number(args.costLimitUsd) : undefined,
|
|
72
73
|
dailyCostLimitUsd: Number(args.dailyCostLimitUsd) > 0 ? Number(args.dailyCostLimitUsd) : undefined,
|
|
73
74
|
tokenLimit: Number(args.tokenLimit) > 0 ? Number(args.tokenLimit) : undefined,
|
|
74
|
-
}
|
|
75
|
+
};
|
|
76
|
+
// Issue #310 / GH #5: strip any undefined properties before store.set()
|
|
77
|
+
for (const k of Object.keys(taskData)) {
|
|
78
|
+
if (taskData[k] === undefined) {
|
|
79
|
+
delete taskData[k];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const task = store.set(taskData);
|
|
75
83
|
scheduler.scheduleTask(task);
|
|
76
84
|
return {
|
|
77
85
|
success: true,
|
|
78
86
|
message: `Task "${task.title}" scheduled successfully (${task.scheduleText})`,
|
|
79
|
-
task
|
|
87
|
+
task: redactTaskSecrets(task),
|
|
80
88
|
};
|
|
81
89
|
}
|
|
82
90
|
|
package/lib/task-patch.js
CHANGED
|
@@ -22,23 +22,27 @@ export function isCodeTypeSwitch(current, nextType) {
|
|
|
22
22
|
return CODE_EXECUTING_TYPES.includes(target) && !CODE_EXECUTING_TYPES.includes(current.type);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** True when a patch introduces code-executing type or shell hooks (#212). */
|
|
25
|
+
/** True when a patch introduces code-executing type or shell hooks (#212, #296). */
|
|
26
26
|
export function isCodeExecutionIntroduced(current, body) {
|
|
27
27
|
if (!current || !body) return false;
|
|
28
28
|
if (body.type !== undefined && isCodeTypeSwitch(current, body.type)) return true;
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const isShellPreflight = (t) => t === 'command' || t === 'shell';
|
|
31
|
+
const currentPreflightType = current.preflightType || 'none';
|
|
32
|
+
const nextPreflightType = body.preflightType !== undefined ? body.preflightType : currentPreflightType;
|
|
33
|
+
|
|
34
|
+
const currentPreflightTarget = current.preflightTarget || current.preflightCommand || '';
|
|
35
|
+
const bodyPreflightTarget = body.preflightTarget !== undefined ? body.preflightTarget : (body.preflightCommand !== undefined ? body.preflightCommand : '');
|
|
36
|
+
const nextPreflightTarget = (body.preflightTarget !== undefined || body.preflightCommand !== undefined) ? bodyPreflightTarget : currentPreflightTarget;
|
|
37
|
+
|
|
38
|
+
const hadPreflight = isShellPreflight(currentPreflightType) && Boolean(String(currentPreflightTarget).trim());
|
|
39
|
+
const hasPreflight = isShellPreflight(nextPreflightType) && Boolean(String(nextPreflightTarget).trim());
|
|
32
40
|
if (hasPreflight && !hadPreflight) return true;
|
|
33
41
|
|
|
34
42
|
const hadSelfHealing = Boolean(current.selfHealingCommand && String(current.selfHealingCommand).trim());
|
|
35
43
|
const hasSelfHealing = Boolean(body.selfHealingCommand && String(body.selfHealingCommand).trim());
|
|
36
44
|
if (hasSelfHealing && !hadSelfHealing) return true;
|
|
37
45
|
|
|
38
|
-
const hadCommand = Boolean(current.command && String(current.command).trim());
|
|
39
|
-
const hasCommand = Boolean(body.command && String(body.command).trim());
|
|
40
|
-
if (hasCommand && !hadCommand && !CODE_EXECUTING_TYPES.includes(current.type)) return true;
|
|
41
|
-
|
|
42
46
|
return false;
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -76,7 +80,7 @@ export function buildTaskPatch(current, body) {
|
|
|
76
80
|
patch.oneShot = Boolean(parsed.isOneShot || (body && body.oneShot));
|
|
77
81
|
if (scheduleChanged) {
|
|
78
82
|
patch.nextRunAt = parsed.isOneShot ? parsed.targetTimestamp : (parsed.nextRun || null);
|
|
79
|
-
patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp :
|
|
83
|
+
patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp : null;
|
|
80
84
|
}
|
|
81
85
|
} catch (err) {
|
|
82
86
|
return { ok: false, error: err.message };
|
|
@@ -91,6 +95,12 @@ export function buildTaskPatch(current, body) {
|
|
|
91
95
|
if (patch.tokenLimit !== undefined) {
|
|
92
96
|
patch.tokenLimit = patch.tokenLimit === null ? null : (Number(patch.tokenLimit) > 0 ? Number(patch.tokenLimit) : null);
|
|
93
97
|
}
|
|
98
|
+
// Issue #310 / GH #5: strip undefined properties from patch
|
|
99
|
+
for (const k of Object.keys(patch)) {
|
|
100
|
+
if (patch[k] === undefined) {
|
|
101
|
+
delete patch[k];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
94
104
|
return { ok: true, patch };
|
|
95
105
|
}
|
|
96
106
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|