@goodandready/dsh-cron 0.2.48 → 0.2.50
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 +2 -2
- package/lib/client.js +258 -65
- package/lib/cron-tool.js +1 -1
- package/lib/http-utils.js +7 -1
- package/lib/runtimes.js +0 -2
- package/lib/store.js +8 -2
- package/lib/task-create.js +11 -4
- package/lib/task-patch.js +12 -2
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID, createHash } from 'node:crypto';
|
|
9
9
|
import { parseScheduleExpression } from './scheduler.js';
|
|
10
|
-
import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
|
|
10
|
+
import { normalizeTaskType, CODE_EXECUTING_TYPES, EXTERNAL_RUNTIMES } from './runtimes.js';
|
|
11
11
|
import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
|
|
12
12
|
import { applyTaskPatch } from './task-patch.js';
|
|
13
13
|
import { MANAGED_BY_CONFIG, configOwnedMessage } from './config-jobs.js';
|
|
@@ -265,7 +265,7 @@ function buildTaskRecord(store, body, taskType, parsed) {
|
|
|
265
265
|
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
|
|
266
266
|
maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
|
|
267
267
|
retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
|
|
268
|
-
permissionPreset: ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current ? (current.permissionPreset || 'default') : 'default'),
|
|
268
|
+
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
|
|
269
269
|
...mergeExecutionFields(current, body, parsed),
|
|
270
270
|
};
|
|
271
271
|
}
|
package/lib/client.js
CHANGED
|
@@ -137,6 +137,7 @@ window.__ModuleLoader__.load({
|
|
|
137
137
|
'modal.params': 'Parameters',
|
|
138
138
|
'modal.history': 'Run history ({count})',
|
|
139
139
|
'modal.cancel': 'Cancel',
|
|
140
|
+
'modal.confirmDiscard': 'Discard unsaved changes?',
|
|
140
141
|
'modal.save': 'Save changes',
|
|
141
142
|
'modal.create': 'Create task',
|
|
142
143
|
'modal.close': 'Close',
|
|
@@ -492,6 +493,7 @@ window.__ModuleLoader__.load({
|
|
|
492
493
|
'modal.params': '配置参数',
|
|
493
494
|
'modal.history': '运行历史 ({count})',
|
|
494
495
|
'modal.cancel': '取消',
|
|
496
|
+
'modal.confirmDiscard': '放弃未保存的内容?',
|
|
495
497
|
'modal.save': '保存修改',
|
|
496
498
|
'modal.create': '创建任务',
|
|
497
499
|
'history.loading': '正在加载历史记录...',
|
|
@@ -1102,6 +1104,54 @@ window.__ModuleLoader__.load({
|
|
|
1102
1104
|
return (typeof t === 'function' ? t : translate)(key, vars);
|
|
1103
1105
|
}
|
|
1104
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* Issue #307: universal fetch with AbortController timeout (default 15s).
|
|
1109
|
+
* Connects with caller signal if supplied and fails closed on timeouts.
|
|
1110
|
+
*/
|
|
1111
|
+
const fetchWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1112
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1113
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1114
|
+
if (options && options.signal && controller) {
|
|
1115
|
+
if (options.signal.aborted) {
|
|
1116
|
+
controller.abort();
|
|
1117
|
+
} else {
|
|
1118
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const res = await fetch(url, {
|
|
1123
|
+
...options,
|
|
1124
|
+
signal: controller ? controller.signal : (options ? options.signal : undefined),
|
|
1125
|
+
});
|
|
1126
|
+
return res;
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
if (err && (err.name === 'AbortError' || controller?.signal?.aborted)) {
|
|
1129
|
+
const timeoutErr = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
1130
|
+
timeoutErr.isTimeout = true;
|
|
1131
|
+
throw timeoutErr;
|
|
1132
|
+
}
|
|
1133
|
+
throw err;
|
|
1134
|
+
} finally {
|
|
1135
|
+
if (timer) clearTimeout(timer);
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Issue #307: universal JSON fetch helper with timeout and aborted body protection.
|
|
1141
|
+
*/
|
|
1142
|
+
const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1143
|
+
const res = await fetchWithTimeout(url, options, timeoutMs);
|
|
1144
|
+
let data = {};
|
|
1145
|
+
try {
|
|
1146
|
+
data = await res.json();
|
|
1147
|
+
} catch (jsonErr) {
|
|
1148
|
+
if (jsonErr && jsonErr.name === 'AbortError') {
|
|
1149
|
+
throw jsonErr;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
return { res, data };
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1105
1155
|
function pickDeliverySettings(source) {
|
|
1106
1156
|
const out = {};
|
|
1107
1157
|
for (const key of DELIVERY_SETTING_KEYS.concat(TEMPLATE_SETTING_KEYS)) {
|
|
@@ -1290,6 +1340,80 @@ window.__ModuleLoader__.load({
|
|
|
1290
1340
|
const [formWorkflowName, setFormWorkflowName] = React.useState('');
|
|
1291
1341
|
const [stats, setStats] = React.useState(null);
|
|
1292
1342
|
|
|
1343
|
+
const initialFormSnapshotRef = React.useRef(null);
|
|
1344
|
+
|
|
1345
|
+
const getFormSnapshot = () => JSON.stringify({
|
|
1346
|
+
title: String(formTitle || '').trim(),
|
|
1347
|
+
schedule: String(formSchedule || '').trim(),
|
|
1348
|
+
type: String(formType || 'llm'),
|
|
1349
|
+
prompt: String(formPrompt || '').trim(),
|
|
1350
|
+
provider: String(formProvider || ''),
|
|
1351
|
+
model: String(formModel || ''),
|
|
1352
|
+
fallbackModel: String(formFallbackModel || ''),
|
|
1353
|
+
silentRule: String(formSilentRule || ''),
|
|
1354
|
+
inspectOnFailure: Boolean(formInspectOnFailure),
|
|
1355
|
+
delivery: String(formDelivery || 'isolated'),
|
|
1356
|
+
notifyTelegram: Boolean(formNotifyTelegram),
|
|
1357
|
+
channels: Array.isArray(formChannels) ? [...formChannels].sort() : [],
|
|
1358
|
+
template: String(formTemplate || '').trim(),
|
|
1359
|
+
onlyOnFailure: Boolean(formOnlyOnFailure),
|
|
1360
|
+
timeoutSeconds: Number(formTimeoutSeconds) || 1800,
|
|
1361
|
+
overlapPolicy: String(formOverlapPolicy || 'skip'),
|
|
1362
|
+
kanbanMode: String(formKanbanMode || 'none'),
|
|
1363
|
+
onSuccess: String(formOnSuccess || '').trim(),
|
|
1364
|
+
onFailure: String(formOnFailure || '').trim(),
|
|
1365
|
+
heartbeatIntervalSeconds: Number(formHeartbeatIntervalSeconds) || 0,
|
|
1366
|
+
gracePeriodSeconds: Number(formGracePeriodSeconds) || 300,
|
|
1367
|
+
preflightType: String(formPreflightType || 'none'),
|
|
1368
|
+
preflightTarget: String(formPreflightTarget || '').trim(),
|
|
1369
|
+
priority: Number(formPriority) || 5,
|
|
1370
|
+
concurrencyGroup: String(formConcurrencyGroup || 'default').trim(),
|
|
1371
|
+
selfHealingCommand: String(formSelfHealingCommand || '').trim(),
|
|
1372
|
+
autoDiagnose: Boolean(formAutoDiagnose),
|
|
1373
|
+
timezone: String(formTimezone || '').trim(),
|
|
1374
|
+
misfirePolicy: String(formMisfirePolicy || 'skip'),
|
|
1375
|
+
maxRetries: Number(formMaxRetries) || 0,
|
|
1376
|
+
permissionPreset: String(formPermissionPreset || 'default'),
|
|
1377
|
+
agentPreset: String(formAgentPreset || '').trim(),
|
|
1378
|
+
targetSessionId: String(formTargetSessionId || '').trim(),
|
|
1379
|
+
targetSessionReset: String(formTargetSessionReset || 'never'),
|
|
1380
|
+
env: String(formEnv || '').trim(),
|
|
1381
|
+
cwd: String(formCwd || '').trim(),
|
|
1382
|
+
workspaceId: String(formWorkspaceId || '').trim(),
|
|
1383
|
+
worktree: Boolean(formWorktree),
|
|
1384
|
+
keepWorktree: Boolean(formKeepWorktree),
|
|
1385
|
+
httpMethod: String(formHttpMethod || 'GET'),
|
|
1386
|
+
httpUrl: String(formHttpUrl || '').trim(),
|
|
1387
|
+
httpHttpHeaders: String(formHttpHeaders || '').trim(),
|
|
1388
|
+
httpBody: String(formHttpBody || '').trim(),
|
|
1389
|
+
sshProfileId: String(formSshProfileId || '').trim(),
|
|
1390
|
+
sshTarget: String(formSshTarget || '').trim(),
|
|
1391
|
+
sshKeyPath: String(formSshKeyPath || '').trim(),
|
|
1392
|
+
dockerImage: String(formDockerImage || '').trim(),
|
|
1393
|
+
nodePath: String(formNodePath || '').trim(),
|
|
1394
|
+
pythonPath: String(formPythonPath || '').trim(),
|
|
1395
|
+
skillName: String(formSkillName || '').trim(),
|
|
1396
|
+
workflowName: String(formWorkflowName || '').trim(),
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
const isFormDirty = () => {
|
|
1400
|
+
if (!manualModalOpen) return false;
|
|
1401
|
+
if (initialFormSnapshotRef.current !== null) {
|
|
1402
|
+
return getFormSnapshot() !== initialFormSnapshotRef.current;
|
|
1403
|
+
}
|
|
1404
|
+
return Boolean(formTitle.trim() || formPrompt.trim());
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
const closeManualModalWithConfirm = () => {
|
|
1408
|
+
if (isFormDirty()) {
|
|
1409
|
+
if (!confirm(T_KEY(t, 'modal.confirmDiscard') || 'Discard unsaved changes?')) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
initialFormSnapshotRef.current = null;
|
|
1414
|
+
setManualModalOpen(false);
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1293
1417
|
// Live execution indicator (#35): a light 1s tick only while some task
|
|
1294
1418
|
// is actually running.
|
|
1295
1419
|
const tasksRef = React.useRef([]);
|
|
@@ -1331,7 +1455,7 @@ window.__ModuleLoader__.load({
|
|
|
1331
1455
|
const onKey = (e) => {
|
|
1332
1456
|
if (e.key === 'Escape') {
|
|
1333
1457
|
if (dropdownOpen) { setDropdownOpen(false); return; }
|
|
1334
|
-
if (manualModalOpen) {
|
|
1458
|
+
if (manualModalOpen) { closeManualModalWithConfirm(); return; }
|
|
1335
1459
|
if (settingsModalOpen) { setSettingsModalOpen(false); return; }
|
|
1336
1460
|
if (dshModalOpen && !dshLoading) { setDshModalOpen(false); return; }
|
|
1337
1461
|
if (onClose) onClose();
|
|
@@ -1391,8 +1515,12 @@ window.__ModuleLoader__.load({
|
|
|
1391
1515
|
const gen = ++fetchGenRef.current;
|
|
1392
1516
|
setLoading(true);
|
|
1393
1517
|
setFetchError(null);
|
|
1518
|
+
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1519
|
+
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
1394
1520
|
try {
|
|
1395
|
-
const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query)
|
|
1521
|
+
const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), {
|
|
1522
|
+
signal: controller ? controller.signal : undefined,
|
|
1523
|
+
});
|
|
1396
1524
|
const data = await res.json();
|
|
1397
1525
|
if (gen !== fetchGenRef.current) return;
|
|
1398
1526
|
if (data && data.ok) {
|
|
@@ -1407,6 +1535,7 @@ window.__ModuleLoader__.load({
|
|
|
1407
1535
|
console.error('[dsh-cron] fetch tasks error:', err);
|
|
1408
1536
|
setFetchError(err.message || T_KEY(t, 'list.loadErrorPrefix'));
|
|
1409
1537
|
} finally {
|
|
1538
|
+
if (timer) clearTimeout(timer);
|
|
1410
1539
|
if (gen === fetchGenRef.current) {
|
|
1411
1540
|
setLoading(false);
|
|
1412
1541
|
}
|
|
@@ -1414,9 +1543,11 @@ window.__ModuleLoader__.load({
|
|
|
1414
1543
|
};
|
|
1415
1544
|
|
|
1416
1545
|
const loadModels = async (providerName) => {
|
|
1546
|
+
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1547
|
+
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
1417
1548
|
try {
|
|
1418
1549
|
const url = providerName ? '/dsh-cron/models?provider=' + encodeURIComponent(providerName) : '/dsh-cron/models';
|
|
1419
|
-
const res = await fetch(url);
|
|
1550
|
+
const res = await fetch(url, { signal: controller ? controller.signal : undefined });
|
|
1420
1551
|
const data = await res.json();
|
|
1421
1552
|
if (data && data.ok) {
|
|
1422
1553
|
if (!providerName) {
|
|
@@ -1431,6 +1562,8 @@ window.__ModuleLoader__.load({
|
|
|
1431
1562
|
}
|
|
1432
1563
|
} catch (err) {
|
|
1433
1564
|
console.error('[dsh-cron] load models error:', err);
|
|
1565
|
+
} finally {
|
|
1566
|
+
if (timer) clearTimeout(timer);
|
|
1434
1567
|
}
|
|
1435
1568
|
};
|
|
1436
1569
|
|
|
@@ -1455,6 +1588,7 @@ window.__ModuleLoader__.load({
|
|
|
1455
1588
|
const fetchOpts = { headers };
|
|
1456
1589
|
if (pollController) fetchOpts.signal = pollController.signal;
|
|
1457
1590
|
|
|
1591
|
+
const pollTimer = pollController ? setTimeout(() => { try { pollController.abort(); } catch (abortErr) { void abortErr; } }, 15000) : null;
|
|
1458
1592
|
fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), fetchOpts)
|
|
1459
1593
|
.then(res => {
|
|
1460
1594
|
if (isDestroyed || pollGen !== fetchGenRef.current) return null;
|
|
@@ -1478,6 +1612,7 @@ window.__ModuleLoader__.load({
|
|
|
1478
1612
|
if (err && err.name === 'AbortError') return;
|
|
1479
1613
|
})
|
|
1480
1614
|
.finally(() => {
|
|
1615
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
1481
1616
|
if (isDestroyed) return;
|
|
1482
1617
|
// Adaptive interval: 30s when tab is hidden, 8s when active (#134)
|
|
1483
1618
|
const intervalMs = (typeof document !== 'undefined' && document.hidden) ? 30000 : 8000;
|
|
@@ -1515,10 +1650,17 @@ window.__ModuleLoader__.load({
|
|
|
1515
1650
|
const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
|
|
1516
1651
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1517
1652
|
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1653
|
+
if (options && options.signal && controller) {
|
|
1654
|
+
if (options.signal.aborted) {
|
|
1655
|
+
controller.abort();
|
|
1656
|
+
} else {
|
|
1657
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1518
1660
|
try {
|
|
1519
1661
|
const res = await fetch(url, {
|
|
1520
1662
|
...options,
|
|
1521
|
-
signal: controller ? controller.signal : options.signal,
|
|
1663
|
+
signal: controller ? controller.signal : (options ? options.signal : undefined),
|
|
1522
1664
|
});
|
|
1523
1665
|
let data = {};
|
|
1524
1666
|
try {
|
|
@@ -1589,8 +1731,7 @@ window.__ModuleLoader__.load({
|
|
|
1589
1731
|
/** #41: duplicate a task; the server copies config, resets run state (#41). */
|
|
1590
1732
|
const handleDryRun = async (taskId) => {
|
|
1591
1733
|
try {
|
|
1592
|
-
const res = await
|
|
1593
|
-
const data = await res.json();
|
|
1734
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/dry-run', { method: 'POST' });
|
|
1594
1735
|
setDryRunResult(data);
|
|
1595
1736
|
} catch (err) {
|
|
1596
1737
|
alert('Dry run error: ' + err.message);
|
|
@@ -1603,8 +1744,7 @@ window.__ModuleLoader__.load({
|
|
|
1603
1744
|
setArchiveLoading(true);
|
|
1604
1745
|
setArchiveModalOpen(true);
|
|
1605
1746
|
try {
|
|
1606
|
-
const res = await
|
|
1607
|
-
const data = await res.json();
|
|
1747
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/archive?limit=15&offset=' + offset);
|
|
1608
1748
|
if (data.ok) {
|
|
1609
1749
|
setArchiveRuns(prev => offset === 0 ? (data.runs || []) : [...prev, ...(data.runs || [])]);
|
|
1610
1750
|
setArchiveTotal(data.total || 0);
|
|
@@ -1618,12 +1758,11 @@ window.__ModuleLoader__.load({
|
|
|
1618
1758
|
|
|
1619
1759
|
const handlePreviewSchedule = async (scheduleExpr) => {
|
|
1620
1760
|
try {
|
|
1621
|
-
const res = await
|
|
1761
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/schedule/preview', {
|
|
1622
1762
|
method: 'POST',
|
|
1623
1763
|
headers: { 'Content-Type': 'application/json' },
|
|
1624
1764
|
body: JSON.stringify({ schedule: scheduleExpr, timezone: formTimezone })
|
|
1625
1765
|
});
|
|
1626
|
-
const data = await res.json();
|
|
1627
1766
|
if (data.ok && data.runs) {
|
|
1628
1767
|
setSchedulePreviewRuns(data.runs);
|
|
1629
1768
|
} else {
|
|
@@ -1636,8 +1775,7 @@ window.__ModuleLoader__.load({
|
|
|
1636
1775
|
|
|
1637
1776
|
const handleDuplicate = async (task) => {
|
|
1638
1777
|
try {
|
|
1639
|
-
const res = await
|
|
1640
|
-
const data = await res.json();
|
|
1778
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/duplicate', { method: 'POST' });
|
|
1641
1779
|
if (!data.ok) {
|
|
1642
1780
|
alert(T_KEY(t, 'duplicate.failed', { error: (data && data.error) || '' }));
|
|
1643
1781
|
return;
|
|
@@ -1678,8 +1816,7 @@ window.__ModuleLoader__.load({
|
|
|
1678
1816
|
const promptMsg = T_KEY(t, 'transfer.exportPrompt') || 'Choose export mode:\nOK = Sanitized (removes custom env vars & headers)\nCancel = Full raw export';
|
|
1679
1817
|
const wantSanitized = confirm(promptMsg);
|
|
1680
1818
|
const exportUrl = '/dsh-cron/tasks/export' + (wantSanitized ? '?sanitize=true' : '');
|
|
1681
|
-
const res = await
|
|
1682
|
-
const data = await res.json();
|
|
1819
|
+
const { res, data } = await fetchJsonWithTimeout(exportUrl);
|
|
1683
1820
|
if (!data.ok) {
|
|
1684
1821
|
alert(T_KEY(t, 'transfer.exportFailed', { error: (data && data.error) || '' }));
|
|
1685
1822
|
return;
|
|
@@ -1717,12 +1854,11 @@ window.__ModuleLoader__.load({
|
|
|
1717
1854
|
alert(T_KEY(t, 'transfer.badFile', { error: parseErr.message }));
|
|
1718
1855
|
return;
|
|
1719
1856
|
}
|
|
1720
|
-
const res = await
|
|
1857
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
|
|
1721
1858
|
method: 'POST',
|
|
1722
1859
|
headers: importHeaders(document_),
|
|
1723
1860
|
body: JSON.stringify({ document: document_, dryRun: true, strategy: importStrategy })
|
|
1724
1861
|
});
|
|
1725
|
-
const data = await res.json();
|
|
1726
1862
|
if (!data.ok) {
|
|
1727
1863
|
alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
|
|
1728
1864
|
return;
|
|
@@ -1740,12 +1876,11 @@ window.__ModuleLoader__.load({
|
|
|
1740
1876
|
if (!importDoc) return;
|
|
1741
1877
|
setTransferBusy(true);
|
|
1742
1878
|
try {
|
|
1743
|
-
const res = await
|
|
1879
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
|
|
1744
1880
|
method: 'POST',
|
|
1745
1881
|
headers: importHeaders(importDoc),
|
|
1746
1882
|
body: JSON.stringify({ document: importDoc, strategy: importStrategy })
|
|
1747
1883
|
});
|
|
1748
|
-
const data = await res.json();
|
|
1749
1884
|
if (!data.ok) {
|
|
1750
1885
|
alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
|
|
1751
1886
|
return;
|
|
@@ -1777,12 +1912,11 @@ window.__ModuleLoader__.load({
|
|
|
1777
1912
|
'If the change makes the task execute code (script/node/python/ssh/docker), confirm it with me explicitly before applying it.',
|
|
1778
1913
|
].filter(Boolean);
|
|
1779
1914
|
try {
|
|
1780
|
-
const res = await
|
|
1915
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
|
|
1781
1916
|
method: 'POST',
|
|
1782
1917
|
headers: { 'Content-Type': 'application/json' },
|
|
1783
1918
|
body: JSON.stringify({ prompt: lines.join(String.fromCharCode(10)) })
|
|
1784
1919
|
});
|
|
1785
|
-
const data = await res.json();
|
|
1786
1920
|
if (!data.ok || !data.sessionId) {
|
|
1787
1921
|
alert(T_KEY(t, 'tune.startFailed', { error: (data && data.error) || '' }));
|
|
1788
1922
|
return;
|
|
@@ -1798,7 +1932,7 @@ window.__ModuleLoader__.load({
|
|
|
1798
1932
|
const handleDelete = async (id) => {
|
|
1799
1933
|
if (!confirm(T_KEY(t, 'confirm.delete'))) return;
|
|
1800
1934
|
try {
|
|
1801
|
-
await
|
|
1935
|
+
await fetchWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
1802
1936
|
fetchTasks();
|
|
1803
1937
|
} catch (err) {
|
|
1804
1938
|
console.debug('[dsh-cron] delete failed', err);
|
|
@@ -1809,8 +1943,7 @@ window.__ModuleLoader__.load({
|
|
|
1809
1943
|
setTestStatus(null);
|
|
1810
1944
|
setSettingsModalOpen(true);
|
|
1811
1945
|
try {
|
|
1812
|
-
const res = await
|
|
1813
|
-
const data = await res.json();
|
|
1946
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings');
|
|
1814
1947
|
if (data && data.ok && data.settings) {
|
|
1815
1948
|
setSettingsBotToken(data.settings.botToken || '');
|
|
1816
1949
|
setSettingsChatId(data.settings.chatId || '');
|
|
@@ -1829,7 +1962,7 @@ window.__ModuleLoader__.load({
|
|
|
1829
1962
|
const handleSaveSettings = async (e) => {
|
|
1830
1963
|
e.preventDefault();
|
|
1831
1964
|
try {
|
|
1832
|
-
const res = await
|
|
1965
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
|
|
1833
1966
|
method: 'POST',
|
|
1834
1967
|
headers: { 'Content-Type': 'application/json' },
|
|
1835
1968
|
body: JSON.stringify(Object.assign({
|
|
@@ -1840,7 +1973,6 @@ window.__ModuleLoader__.load({
|
|
|
1840
1973
|
kanbanBaseUrl: settingsKanbanUrl,
|
|
1841
1974
|
}, settingsDelivery))
|
|
1842
1975
|
});
|
|
1843
|
-
const data = await res.json();
|
|
1844
1976
|
if (data && data.ok) {
|
|
1845
1977
|
setSettingsModalOpen(false);
|
|
1846
1978
|
} else {
|
|
@@ -1881,7 +2013,7 @@ window.__ModuleLoader__.load({
|
|
|
1881
2013
|
|
|
1882
2014
|
const handleTestTelegram = async () => { setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
1883
2015
|
try {
|
|
1884
|
-
const res = await
|
|
2016
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
|
|
1885
2017
|
method: 'POST',
|
|
1886
2018
|
headers: { 'Content-Type': 'application/json' },
|
|
1887
2019
|
body: JSON.stringify({
|
|
@@ -1889,7 +2021,6 @@ window.__ModuleLoader__.load({
|
|
|
1889
2021
|
chatId: settingsChatId
|
|
1890
2022
|
})
|
|
1891
2023
|
});
|
|
1892
|
-
const data = await res.json();
|
|
1893
2024
|
if (data && data.ok) {
|
|
1894
2025
|
setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
|
|
1895
2026
|
} else {
|
|
@@ -1963,14 +2094,66 @@ window.__ModuleLoader__.load({
|
|
|
1963
2094
|
setFormModel(defaultModelInfo?.model || '');
|
|
1964
2095
|
if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
1965
2096
|
}
|
|
2097
|
+
initialFormSnapshotRef.current = JSON.stringify({
|
|
2098
|
+
title: String((preset && preset.title) || '').trim(),
|
|
2099
|
+
schedule: String((preset && preset.schedule) || (preset ? '' : '0 9 * * 1-5')).trim(),
|
|
2100
|
+
type: String((preset && preset.type) || 'llm'),
|
|
2101
|
+
prompt: String((preset && preset.prompt) || '').trim(),
|
|
2102
|
+
provider: String(defaultModelInfo?.provider || ''),
|
|
2103
|
+
model: String(defaultModelInfo?.model || ''),
|
|
2104
|
+
fallbackModel: String((preset && preset.fallbackModel) || '').trim(),
|
|
2105
|
+
silentRule: String((preset && preset.silentRule) || '').trim(),
|
|
2106
|
+
inspectOnFailure: Boolean(preset && preset.inspectOnFailure),
|
|
2107
|
+
delivery: 'isolated',
|
|
2108
|
+
notifyTelegram: false,
|
|
2109
|
+
channels: Array.isArray(preset && preset.channels) ? [...preset.channels].filter((c) => DELIVERY_CHANNELS.includes(c)).sort() : [],
|
|
2110
|
+
template: '',
|
|
2111
|
+
onlyOnFailure: Boolean(preset && preset.onlyOnFailure),
|
|
2112
|
+
timeoutSeconds: 1800,
|
|
2113
|
+
overlapPolicy: 'skip',
|
|
2114
|
+
kanbanMode: 'none',
|
|
2115
|
+
onSuccess: '',
|
|
2116
|
+
onFailure: '',
|
|
2117
|
+
heartbeatIntervalSeconds: 0,
|
|
2118
|
+
gracePeriodSeconds: 300,
|
|
2119
|
+
preflightType: 'none',
|
|
2120
|
+
preflightTarget: '',
|
|
2121
|
+
priority: 5,
|
|
2122
|
+
concurrencyGroup: 'default',
|
|
2123
|
+
selfHealingCommand: '',
|
|
2124
|
+
autoDiagnose: false,
|
|
2125
|
+
timezone: '',
|
|
2126
|
+
misfirePolicy: 'skip',
|
|
2127
|
+
maxRetries: 0,
|
|
2128
|
+
permissionPreset: 'default',
|
|
2129
|
+
agentPreset: '',
|
|
2130
|
+
targetSessionId: '',
|
|
2131
|
+
targetSessionReset: 'never',
|
|
2132
|
+
env: '',
|
|
2133
|
+
cwd: '',
|
|
2134
|
+
workspaceId: '',
|
|
2135
|
+
worktree: false,
|
|
2136
|
+
keepWorktree: false,
|
|
2137
|
+
httpMethod: 'GET',
|
|
2138
|
+
httpUrl: '',
|
|
2139
|
+
httpHttpHeaders: '',
|
|
2140
|
+
httpBody: '',
|
|
2141
|
+
sshProfileId: '',
|
|
2142
|
+
sshTarget: '',
|
|
2143
|
+
sshKeyPath: '',
|
|
2144
|
+
dockerImage: '',
|
|
2145
|
+
nodePath: '',
|
|
2146
|
+
pythonPath: '',
|
|
2147
|
+
skillName: '',
|
|
2148
|
+
workflowName: '',
|
|
2149
|
+
});
|
|
1966
2150
|
setManualModalOpen(true);
|
|
1967
2151
|
};
|
|
1968
2152
|
|
|
1969
2153
|
const loadTaskHistory = async (taskId) => {
|
|
1970
2154
|
setHistoryLoading(true);
|
|
1971
2155
|
try {
|
|
1972
|
-
const res = await
|
|
1973
|
-
const data = await res.json();
|
|
2156
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/history');
|
|
1974
2157
|
if (data.ok) {
|
|
1975
2158
|
setTaskHistory(data.history || []);
|
|
1976
2159
|
}
|
|
@@ -2077,7 +2260,7 @@ window.__ModuleLoader__.load({
|
|
|
2077
2260
|
timezone: formTimezone.trim(),
|
|
2078
2261
|
misfirePolicy: formMisfirePolicy,
|
|
2079
2262
|
maxRetries: Number(formMaxRetries) || 0,
|
|
2080
|
-
permissionPreset: formPermissionPreset,
|
|
2263
|
+
permissionPreset: isAgentType ? formPermissionPreset : 'default',
|
|
2081
2264
|
agentPreset: formAgentPreset.trim() || undefined,
|
|
2082
2265
|
targetSessionId: formTargetSessionId.trim() || undefined,
|
|
2083
2266
|
targetSessionReset: formTargetSessionReset || 'never',
|
|
@@ -2119,13 +2302,13 @@ window.__ModuleLoader__.load({
|
|
|
2119
2302
|
Boolean(payload.preflightCommand && payload.preflightCommand.trim()) ||
|
|
2120
2303
|
Boolean(payload.selfHealingCommand && payload.selfHealingCommand.trim());
|
|
2121
2304
|
if (hasCodeExec) headers[SCRIPT_CONFIRM_HEADER] = 'script';
|
|
2122
|
-
const res = await
|
|
2305
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks', {
|
|
2123
2306
|
method: 'POST',
|
|
2124
2307
|
headers,
|
|
2125
2308
|
body: JSON.stringify(payload)
|
|
2126
2309
|
});
|
|
2127
|
-
const data = await res.json();
|
|
2128
2310
|
if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
|
|
2311
|
+
initialFormSnapshotRef.current = null;
|
|
2129
2312
|
setManualModalOpen(false);
|
|
2130
2313
|
fetchTasks();
|
|
2131
2314
|
} catch (err) { alert(err.message); }
|
|
@@ -2143,12 +2326,11 @@ window.__ModuleLoader__.load({
|
|
|
2143
2326
|
|
|
2144
2327
|
setDshLoading(true);
|
|
2145
2328
|
try {
|
|
2146
|
-
const res = await
|
|
2329
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
|
|
2147
2330
|
method: 'POST',
|
|
2148
2331
|
headers: { 'Content-Type': 'application/json' },
|
|
2149
2332
|
body: JSON.stringify({ prompt: text })
|
|
2150
2333
|
});
|
|
2151
|
-
const data = await res.json();
|
|
2152
2334
|
if (!data.ok || !data.sessionId) {
|
|
2153
2335
|
alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
|
|
2154
2336
|
setDshLoading(false);
|
|
@@ -2511,7 +2693,12 @@ window.__ModuleLoader__.load({
|
|
|
2511
2693
|
),
|
|
2512
2694
|
|
|
2513
2695
|
// 1. Manual creation / Edit modal
|
|
2514
|
-
manualModalOpen && React.createElement('div', {
|
|
2696
|
+
manualModalOpen && React.createElement('div', {
|
|
2697
|
+
className: 'dsh-cron-modal-overlay',
|
|
2698
|
+
onClick: (e) => {
|
|
2699
|
+
if (e.target === e.currentTarget) closeManualModalWithConfirm();
|
|
2700
|
+
}
|
|
2701
|
+
},
|
|
2515
2702
|
React.createElement('div', {
|
|
2516
2703
|
className: 'dsh-cron-modal dsh-cron-modal-scroll',
|
|
2517
2704
|
style: { maxWidth: '640px' },
|
|
@@ -2614,7 +2801,7 @@ window.__ModuleLoader__.load({
|
|
|
2614
2801
|
React.createElement('button', {
|
|
2615
2802
|
type: 'button',
|
|
2616
2803
|
className: 'dsh-cron-btn-primary',
|
|
2617
|
-
onClick:
|
|
2804
|
+
onClick: closeManualModalWithConfirm
|
|
2618
2805
|
}, T_KEY(t, 'modal.close'))
|
|
2619
2806
|
)
|
|
2620
2807
|
) :
|
|
@@ -2728,7 +2915,32 @@ window.__ModuleLoader__.load({
|
|
|
2728
2915
|
)
|
|
2729
2916
|
)
|
|
2730
2917
|
),
|
|
2731
|
-
|
|
2918
|
+
AGENT_FORM_TYPES.includes(formType) ? (
|
|
2919
|
+
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
2920
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2921
|
+
React.createElement('label', null, T_KEY(t, 'form.retriesLabel')),
|
|
2922
|
+
React.createElement('input', {
|
|
2923
|
+
type: 'number',
|
|
2924
|
+
min: 0,
|
|
2925
|
+
value: formMaxRetries,
|
|
2926
|
+
placeholder: '0',
|
|
2927
|
+
onChange: (e) => setFormMaxRetries(e.target.value)
|
|
2928
|
+
})
|
|
2929
|
+
),
|
|
2930
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2931
|
+
React.createElement('label', null, T_KEY(t, 'form.permissionLabel')),
|
|
2932
|
+
React.createElement('select', {
|
|
2933
|
+
value: formPermissionPreset,
|
|
2934
|
+
onChange: (e) => setFormPermissionPreset(e.target.value)
|
|
2935
|
+
},
|
|
2936
|
+
React.createElement('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
|
|
2937
|
+
React.createElement('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
|
|
2938
|
+
React.createElement('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
|
|
2939
|
+
React.createElement('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
|
|
2940
|
+
)
|
|
2941
|
+
)
|
|
2942
|
+
)
|
|
2943
|
+
) : (
|
|
2732
2944
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2733
2945
|
React.createElement('label', null, T_KEY(t, 'form.retriesLabel')),
|
|
2734
2946
|
React.createElement('input', {
|
|
@@ -2738,18 +2950,6 @@ window.__ModuleLoader__.load({
|
|
|
2738
2950
|
placeholder: '0',
|
|
2739
2951
|
onChange: (e) => setFormMaxRetries(e.target.value)
|
|
2740
2952
|
})
|
|
2741
|
-
),
|
|
2742
|
-
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
2743
|
-
React.createElement('label', null, T_KEY(t, 'form.permissionLabel')),
|
|
2744
|
-
React.createElement('select', {
|
|
2745
|
-
value: formPermissionPreset,
|
|
2746
|
-
onChange: (e) => setFormPermissionPreset(e.target.value)
|
|
2747
|
-
},
|
|
2748
|
-
React.createElement('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
|
|
2749
|
-
React.createElement('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
|
|
2750
|
-
React.createElement('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
|
|
2751
|
-
React.createElement('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
|
|
2752
|
-
)
|
|
2753
2953
|
)
|
|
2754
2954
|
),
|
|
2755
2955
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
@@ -3197,7 +3397,7 @@ window.__ModuleLoader__.load({
|
|
|
3197
3397
|
React.createElement('button', {
|
|
3198
3398
|
type: 'button',
|
|
3199
3399
|
className: 'dsh-cron-btn-secondary',
|
|
3200
|
-
onClick:
|
|
3400
|
+
onClick: closeManualModalWithConfirm
|
|
3201
3401
|
}, T_KEY(t, 'modal.cancel')),
|
|
3202
3402
|
React.createElement('button', {
|
|
3203
3403
|
type: 'submit',
|
|
@@ -3532,8 +3732,7 @@ window.__ModuleLoader__.load({
|
|
|
3532
3732
|
const checkUpdates = async () => {
|
|
3533
3733
|
setUpdateState(prev => ({ ...prev, checking: true, error: null, notice: null }));
|
|
3534
3734
|
try {
|
|
3535
|
-
const res = await
|
|
3536
|
-
const data = await res.json();
|
|
3735
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update');
|
|
3537
3736
|
if (data && data.currentVersion) {
|
|
3538
3737
|
setUpdateState(prev => ({
|
|
3539
3738
|
...prev,
|
|
@@ -3554,11 +3753,10 @@ window.__ModuleLoader__.load({
|
|
|
3554
3753
|
const runUpdate = async () => {
|
|
3555
3754
|
setUpdateState(prev => ({ ...prev, updating: true, error: null, notice: null }));
|
|
3556
3755
|
try {
|
|
3557
|
-
const res = await
|
|
3756
|
+
const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update', {
|
|
3558
3757
|
method: 'POST',
|
|
3559
3758
|
headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' }
|
|
3560
3759
|
});
|
|
3561
|
-
const data = await res.json();
|
|
3562
3760
|
if (data && data.updated) {
|
|
3563
3761
|
setUpdateState(prev => ({
|
|
3564
3762
|
...prev,
|
|
@@ -3587,9 +3785,8 @@ window.__ModuleLoader__.load({
|
|
|
3587
3785
|
React.useEffect(() => {
|
|
3588
3786
|
let active = true;
|
|
3589
3787
|
setLoadState('loading');
|
|
3590
|
-
|
|
3591
|
-
.then(
|
|
3592
|
-
.then(data => {
|
|
3788
|
+
fetchJsonWithTimeout('/dsh-cron/settings')
|
|
3789
|
+
.then(({ data }) => {
|
|
3593
3790
|
if (!active) return;
|
|
3594
3791
|
if (data && data.ok && data.settings) {
|
|
3595
3792
|
setToken(data.settings.botToken || '');
|
|
@@ -3615,7 +3812,7 @@ window.__ModuleLoader__.load({
|
|
|
3615
3812
|
const saveSettings = async (e) => {
|
|
3616
3813
|
if (e) e.preventDefault();
|
|
3617
3814
|
try {
|
|
3618
|
-
const res = await
|
|
3815
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
|
|
3619
3816
|
method: 'POST',
|
|
3620
3817
|
headers: { 'Content-Type': 'application/json' },
|
|
3621
3818
|
body: JSON.stringify(Object.assign({
|
|
@@ -3627,7 +3824,6 @@ window.__ModuleLoader__.load({
|
|
|
3627
3824
|
kanbanBaseUrl: kanbanUrl
|
|
3628
3825
|
}, delivery))
|
|
3629
3826
|
});
|
|
3630
|
-
const data = await res.json();
|
|
3631
3827
|
if (data && data.ok) {
|
|
3632
3828
|
setSaved(true);
|
|
3633
3829
|
setTimeout(() => setSaved(false), 2500);
|
|
@@ -3640,12 +3836,11 @@ window.__ModuleLoader__.load({
|
|
|
3640
3836
|
const testTelegram = async () => {
|
|
3641
3837
|
setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
3642
3838
|
try {
|
|
3643
|
-
const res = await
|
|
3839
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
|
|
3644
3840
|
method: 'POST',
|
|
3645
3841
|
headers: { 'Content-Type': 'application/json' },
|
|
3646
3842
|
body: JSON.stringify({ botToken: token, chatId })
|
|
3647
3843
|
});
|
|
3648
|
-
const data = await res.json();
|
|
3649
3844
|
if (data && data.ok) {
|
|
3650
3845
|
setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
|
|
3651
3846
|
} else {
|
|
@@ -3659,12 +3854,11 @@ window.__ModuleLoader__.load({
|
|
|
3659
3854
|
const testKanban = async () => {
|
|
3660
3855
|
setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
|
|
3661
3856
|
try {
|
|
3662
|
-
const res = await
|
|
3857
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/kanban/test', {
|
|
3663
3858
|
method: 'POST',
|
|
3664
3859
|
headers: { 'Content-Type': 'application/json' },
|
|
3665
3860
|
body: JSON.stringify({ kanbanBaseUrl: kanbanUrl })
|
|
3666
3861
|
});
|
|
3667
|
-
const data = await res.json();
|
|
3668
3862
|
if (data && data.ok) {
|
|
3669
3863
|
setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
|
|
3670
3864
|
} else {
|
|
@@ -3971,8 +4165,7 @@ window.__ModuleLoader__.load({
|
|
|
3971
4165
|
|
|
3972
4166
|
async function loadSidebarJobs(state) {
|
|
3973
4167
|
try {
|
|
3974
|
-
const res = await
|
|
3975
|
-
const data = await res.json();
|
|
4168
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks?status=active');
|
|
3976
4169
|
if (data && data.ok) renderSidebarJobs(state, data.tasks || []);
|
|
3977
4170
|
} catch (err) {
|
|
3978
4171
|
console.debug('[dsh-cron] load sidebar jobs failed', err);
|
package/lib/cron-tool.js
CHANGED
|
@@ -150,7 +150,7 @@ export const cronToolParameters = {
|
|
|
150
150
|
permissionPreset: {
|
|
151
151
|
type: 'string',
|
|
152
152
|
enum: ['default', 'read-only', 'workspace-write', 'full'],
|
|
153
|
-
description: 'Permission preset applied to the task session',
|
|
153
|
+
description: 'Permission preset applied to the task session (only used for agent session tasks: llm, skill, workflow)',
|
|
154
154
|
},
|
|
155
155
|
kanbanMode: {
|
|
156
156
|
type: 'string',
|
package/lib/http-utils.js
CHANGED
|
@@ -180,7 +180,13 @@ export async function readBody(req, res) {
|
|
|
180
180
|
*/
|
|
181
181
|
export function redactTaskSecrets(task) {
|
|
182
182
|
if (!task || typeof task !== 'object') return task;
|
|
183
|
-
|
|
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
|
+
}
|
|
184
190
|
|
|
185
191
|
if (clone.env && typeof clone.env === 'object') {
|
|
186
192
|
const redactedEnv = {};
|
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();
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeTaskType } from './runtimes.js';
|
|
1
|
+
import { normalizeTaskType, EXTERNAL_RUNTIMES } 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';
|
|
@@ -9,7 +9,7 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
9
9
|
const typeError = validateTaskType(taskType, args);
|
|
10
10
|
if (typeError) throw new Error(typeError);
|
|
11
11
|
const parsed = parseScheduleExpression(args.schedule);
|
|
12
|
-
const
|
|
12
|
+
const taskData = {
|
|
13
13
|
title: args.title,
|
|
14
14
|
schedule: parsed.cronPattern || args.schedule,
|
|
15
15
|
scheduleText: parsed.humanText,
|
|
@@ -44,7 +44,7 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
44
44
|
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(args.misfirePolicy) ? args.misfirePolicy : 'skip',
|
|
45
45
|
maxRetries: Number(args.maxRetries) > 0 ? Number(args.maxRetries) : 0,
|
|
46
46
|
retryBackoffMs: Number(args.retryBackoffMs) > 0 ? Number(args.retryBackoffMs) : 30000,
|
|
47
|
-
permissionPreset: ['default', 'read-only', 'workspace-write', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
|
|
47
|
+
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
|
|
48
48
|
kanbanMode: args.kanbanMode || 'none',
|
|
49
49
|
channels: Array.isArray(args.channels) ? args.channels.filter((c) => CHANNEL_IDS.includes(c)) : undefined,
|
|
50
50
|
template: args.template ? String(args.template) : '',
|
|
@@ -72,7 +72,14 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
72
72
|
costLimitUsd: Number(args.costLimitUsd) > 0 ? Number(args.costLimitUsd) : undefined,
|
|
73
73
|
dailyCostLimitUsd: Number(args.dailyCostLimitUsd) > 0 ? Number(args.dailyCostLimitUsd) : undefined,
|
|
74
74
|
tokenLimit: Number(args.tokenLimit) > 0 ? Number(args.tokenLimit) : undefined,
|
|
75
|
-
}
|
|
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);
|
|
76
83
|
scheduler.scheduleTask(task);
|
|
77
84
|
return {
|
|
78
85
|
success: true,
|
package/lib/task-patch.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { parseScheduleExpression } from './scheduler.js';
|
|
12
|
-
import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
|
|
12
|
+
import { normalizeTaskType, CODE_EXECUTING_TYPES, EXTERNAL_RUNTIMES } from './runtimes.js';
|
|
13
13
|
import { PATCHABLE_TASK_FIELDS, validateTaskType } from './task-transfer.js';
|
|
14
14
|
import { pickPatchableFields, restoreTaskSecrets } from './http-utils.js';
|
|
15
15
|
import { unknownChannelIds } from './channels.js';
|
|
@@ -80,7 +80,7 @@ export function buildTaskPatch(current, body) {
|
|
|
80
80
|
patch.oneShot = Boolean(parsed.isOneShot || (body && body.oneShot));
|
|
81
81
|
if (scheduleChanged) {
|
|
82
82
|
patch.nextRunAt = parsed.isOneShot ? parsed.targetTimestamp : (parsed.nextRun || null);
|
|
83
|
-
patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp :
|
|
83
|
+
patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp : null;
|
|
84
84
|
}
|
|
85
85
|
} catch (err) {
|
|
86
86
|
return { ok: false, error: err.message };
|
|
@@ -95,6 +95,16 @@ export function buildTaskPatch(current, body) {
|
|
|
95
95
|
if (patch.tokenLimit !== undefined) {
|
|
96
96
|
patch.tokenLimit = patch.tokenLimit === null ? null : (Number(patch.tokenLimit) > 0 ? Number(patch.tokenLimit) : null);
|
|
97
97
|
}
|
|
98
|
+
const effectiveType = patch.type !== undefined ? normalizeTaskType(patch.type) : current.type;
|
|
99
|
+
if (EXTERNAL_RUNTIMES.includes(effectiveType) && patch.permissionPreset !== undefined) {
|
|
100
|
+
patch.permissionPreset = 'default';
|
|
101
|
+
}
|
|
102
|
+
// Issue #310 / GH #5: strip undefined properties from patch
|
|
103
|
+
for (const k of Object.keys(patch)) {
|
|
104
|
+
if (patch[k] === undefined) {
|
|
105
|
+
delete patch[k];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
98
108
|
return { ok: true, patch };
|
|
99
109
|
}
|
|
100
110
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.50",
|
|
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",
|