@goodandready/dsh-cron 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -14
- package/docs/README.ru.md +73 -13
- package/docs/README.zh.md +78 -13
- package/docs/design/DESIGN.md +13 -7
- package/lib/channels.js +422 -0
- package/lib/client.js +594 -23
- package/lib/index.js +210 -16
- package/lib/integrations.js +3 -1
- package/lib/runner.js +185 -28
- package/lib/runtimes.js +174 -0
- package/lib/scheduler.js +66 -60
- package/lib/secrets.js +123 -0
- package/lib/store.js +91 -21
- package/lib/telegram.js +20 -4
- package/lib/templates.js +88 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -53,7 +53,7 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
'actions.delete': 'Delete task',
|
|
54
54
|
'actions.pause': 'Pause',
|
|
55
55
|
'actions.resume': 'Resume',
|
|
56
|
-
'actions.settings': '
|
|
56
|
+
'actions.settings': 'Notifications & channels',
|
|
57
57
|
'search.placeholder': 'Search scheduled tasks',
|
|
58
58
|
'stats.activeTasks': 'Active tasks',
|
|
59
59
|
'stats.totalRuns': 'Total runs',
|
|
@@ -94,7 +94,32 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
'form.namePlaceholder': 'For example: dsh-tags-watch',
|
|
95
95
|
'form.typeLabel': 'Execution type',
|
|
96
96
|
'form.typeLlm': 'LLM (AI agent)',
|
|
97
|
-
'form.typeScript': '
|
|
97
|
+
'form.typeScript': 'Shell command / script',
|
|
98
|
+
'form.typeNode': 'Node.js',
|
|
99
|
+
'form.typePython': 'Python',
|
|
100
|
+
'form.typeHttp': 'HTTP request / webhook',
|
|
101
|
+
'form.typeSsh': 'Remote SSH',
|
|
102
|
+
'form.typeDocker': 'Docker container',
|
|
103
|
+
'form.typeSkill': 'DSH Skill',
|
|
104
|
+
'form.typeWorkflow': 'DSH Workflow',
|
|
105
|
+
'form.envLabel': 'Environment variables (KEY=VALUE per line)',
|
|
106
|
+
'form.envPlaceholder': 'NODE_ENV=production\nTZ=Europe/Berlin',
|
|
107
|
+
'form.envHint': 'Applied to non-LLM runtimes only. Do not store secrets here.',
|
|
108
|
+
'form.cwdLabel': 'Working directory (optional)',
|
|
109
|
+
'form.workspaceLabel': 'Workspace id or path (optional)',
|
|
110
|
+
'form.worktreeLabel': 'Run in an isolated git worktree',
|
|
111
|
+
'form.keepWorktreeLabel': 'Keep the worktree after the run',
|
|
112
|
+
'form.httpMethodLabel': 'HTTP method',
|
|
113
|
+
'form.httpUrlLabel': 'URL',
|
|
114
|
+
'form.httpHeadersLabel': 'Headers (JSON, optional)',
|
|
115
|
+
'form.httpBodyLabel': 'Body (optional)',
|
|
116
|
+
'form.sshProfileLabel': 'dsh-remote-workspace profile id',
|
|
117
|
+
'form.sshProfileHint': 'Preferred: host and credentials stay in the remote-workspace plugin.',
|
|
118
|
+
'form.sshTargetLabel': 'Fallback target (user@host, optional)',
|
|
119
|
+
'form.sshKeyLabel': 'Private key path (fallback, optional)',
|
|
120
|
+
'form.dockerImageLabel': 'Container image',
|
|
121
|
+
'form.skillNameLabel': 'Skill name',
|
|
122
|
+
'form.workflowNameLabel': 'Workflow name',
|
|
98
123
|
'form.scheduleLabel': 'Schedule (cron or interval)',
|
|
99
124
|
'form.schedulePlaceholder': '0 */6 * * * or every 2h or in 30m or at: 2026-09-05T15:00:00Z',
|
|
100
125
|
'form.timeoutLabel': 'Execution timeout (seconds)',
|
|
@@ -131,15 +156,62 @@ window.__ModuleLoader__.load({
|
|
|
131
156
|
'form.notifyTitle': 'Telegram notifications',
|
|
132
157
|
'form.notifyTelegram': 'Send the run report to Telegram',
|
|
133
158
|
'form.notifyOnlyFailure': 'Only on failures (onlyOnFailure / silent on success)',
|
|
159
|
+
'form.channelsTitle': 'Delivery channels',
|
|
160
|
+
'form.channelsHint': 'Pick the channels for this task. An explicit selection overrides the Telegram switch above; leave everything unchecked to fall back to it.',
|
|
161
|
+
'form.templateLabel': 'Message template (optional)',
|
|
162
|
+
'form.templatePlaceholder': '⏰ {title} — {status} in {duration}',
|
|
163
|
+
'form.templateHint': 'Variables: {title} {id} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost}. Empty = built-in text.',
|
|
164
|
+
'form.channelTelegram': 'Telegram',
|
|
165
|
+
'form.channelKanban': 'dsh-kanban card',
|
|
166
|
+
'form.channelDiscord': 'Discord',
|
|
167
|
+
'form.channelSlack': 'Slack',
|
|
168
|
+
'form.channelNtfy': 'ntfy',
|
|
169
|
+
'form.channelBark': 'Bark',
|
|
170
|
+
'form.channelPushplus': 'PushPlus',
|
|
171
|
+
'form.channelEmail': 'Email',
|
|
172
|
+
'form.channelTts': 'Voice (dsh-tts)',
|
|
173
|
+
'form.channelGitea': 'Gitea issue',
|
|
174
|
+
'settings.deliverySection': 'Delivery channels',
|
|
175
|
+
'settings.secretsSection': 'Credentials (references)',
|
|
176
|
+
'settings.secretsHint': 'Enter the NAME of a DSH credential, not the secret itself. Secrets stay in the credentials store.',
|
|
177
|
+
'settings.telegramSection': 'Telegram',
|
|
178
|
+
'settings.templatesSection': 'Message templates',
|
|
179
|
+
'settings.channelTemplateLabel': 'Template for {channel}',
|
|
180
|
+
'settings.botTokenRefLabel': 'Bot token credential name',
|
|
181
|
+
'settings.botTokenRefPlaceholder': 'e.g. TELEGRAM_BOT_TOKEN',
|
|
182
|
+
'settings.discordLabel': 'Discord webhook URL',
|
|
183
|
+
'settings.slackLabel': 'Slack webhook URL',
|
|
184
|
+
'settings.ntfyUrlLabel': 'ntfy server URL',
|
|
185
|
+
'settings.ntfyTopicLabel': 'ntfy topic',
|
|
186
|
+
'settings.ntfyTokenRefLabel': 'ntfy token credential name',
|
|
187
|
+
'settings.barkServerLabel': 'Bark server URL',
|
|
188
|
+
'settings.barkKeyLabel': 'Bark device key',
|
|
189
|
+
'settings.pushplusTokenRefLabel': 'PushPlus token credential name',
|
|
190
|
+
'settings.pushplusUrlLabel': 'PushPlus endpoint',
|
|
191
|
+
'settings.smtpHostLabel': 'SMTP host',
|
|
192
|
+
'settings.smtpPortLabel': 'SMTP port',
|
|
193
|
+
'settings.smtpUserLabel': 'SMTP user',
|
|
194
|
+
'settings.smtpPasswordRefLabel': 'SMTP password credential name',
|
|
195
|
+
'settings.smtpFromLabel': 'From address',
|
|
196
|
+
'settings.smtpToLabel': 'Recipients (comma separated)',
|
|
197
|
+
'settings.smtpSecureLabel': 'SMTP over TLS (implicit)',
|
|
198
|
+
'settings.ttsBaseUrlLabel': 'dsh-tts base URL',
|
|
199
|
+
'settings.giteaBaseUrlLabel': 'Gitea base URL',
|
|
200
|
+
'settings.giteaRepoLabel': 'Gitea repository (owner/repo)',
|
|
201
|
+
'settings.giteaTokenRefLabel': 'Gitea token credential name',
|
|
202
|
+
'settings.templateLabel': 'Default message template',
|
|
203
|
+
'settings.templateHint': 'Applies to every channel without its own template. Empty = built-in text.',
|
|
204
|
+
'settings.deliveryTimeoutMsLabel': 'Delivery timeout per channel (ms)',
|
|
205
|
+
'settings.secretsOnCard': 'Credentials referenced from the settings panel are shown masked and stored by name.',
|
|
134
206
|
'form.nextRunLabel': 'Next run: ',
|
|
135
207
|
'form.notScheduled': 'not scheduled',
|
|
136
208
|
'settings.loading': 'Loading settings...',
|
|
137
209
|
'settings.unavailable': 'Settings are unavailable right now.',
|
|
138
210
|
'settings.retry': 'Retry',
|
|
139
211
|
'settings.cardTitle': '⏰ Task scheduler (Cron)',
|
|
140
|
-
'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios and Telegram
|
|
212
|
+
'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios, and delivery of run reports to Telegram, Discord, Slack, ntfy, Bark, PushPlus, email, voice and Gitea.',
|
|
141
213
|
'settings.openPanel': 'Open task panel',
|
|
142
|
-
'settings.modalTitle': '
|
|
214
|
+
'settings.modalTitle': 'Notifications & delivery channels',
|
|
143
215
|
'settings.tokenLabel': 'Telegram Bot Token',
|
|
144
216
|
'settings.tokenPlaceholder': '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
|
|
145
217
|
'settings.tokenPlaceholderDefault': 'Using the bot configured in DSH settings (or enter your own)',
|
|
@@ -231,6 +303,169 @@ window.__ModuleLoader__.load({
|
|
|
231
303
|
}
|
|
232
304
|
const ErrorBoundary = createErrorBoundary();
|
|
233
305
|
|
|
306
|
+
const AGENT_FORM_TYPES = ['llm', 'skill', 'workflow'];
|
|
307
|
+
const CODE_FORM_TYPES = ['script', 'node', 'python', 'ssh', 'docker'];
|
|
308
|
+
|
|
309
|
+
// Delivery channels (#26) — must stay in sync with lib/channels.js.
|
|
310
|
+
const DELIVERY_CHANNELS = ['telegram', 'kanban', 'discord', 'slack', 'ntfy', 'bark', 'pushplus', 'email', 'tts', 'gitea'];
|
|
311
|
+
const CHANNEL_LABEL_KEYS = {
|
|
312
|
+
telegram: 'form.channelTelegram',
|
|
313
|
+
kanban: 'form.channelKanban',
|
|
314
|
+
discord: 'form.channelDiscord',
|
|
315
|
+
slack: 'form.channelSlack',
|
|
316
|
+
ntfy: 'form.channelNtfy',
|
|
317
|
+
bark: 'form.channelBark',
|
|
318
|
+
pushplus: 'form.channelPushplus',
|
|
319
|
+
email: 'form.channelEmail',
|
|
320
|
+
tts: 'form.channelTts',
|
|
321
|
+
gitea: 'form.channelGitea',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Non-secret delivery settings shown in the settings panel. Secrets are
|
|
325
|
+
// referenced by credential name only (#51) — never typed as values.
|
|
326
|
+
const DELIVERY_SETTING_FIELDS = [
|
|
327
|
+
{ key: 'botTokenRef', labelKey: 'settings.botTokenRefLabel', placeholderKey: 'settings.botTokenRefPlaceholder', group: 'secrets' },
|
|
328
|
+
{ key: 'ntfyTokenRef', labelKey: 'settings.ntfyTokenRefLabel', group: 'secrets' },
|
|
329
|
+
{ key: 'pushplusTokenRef', labelKey: 'settings.pushplusTokenRefLabel', group: 'secrets' },
|
|
330
|
+
{ key: 'smtpPasswordRef', labelKey: 'settings.smtpPasswordRefLabel', group: 'secrets' },
|
|
331
|
+
{ key: 'giteaTokenRef', labelKey: 'settings.giteaTokenRefLabel', group: 'secrets' },
|
|
332
|
+
{ key: 'discordWebhookUrl', labelKey: 'settings.discordLabel', group: 'channels' },
|
|
333
|
+
{ key: 'slackWebhookUrl', labelKey: 'settings.slackLabel', group: 'channels' },
|
|
334
|
+
{ key: 'ntfyUrl', labelKey: 'settings.ntfyUrlLabel', group: 'channels' },
|
|
335
|
+
{ key: 'ntfyTopic', labelKey: 'settings.ntfyTopicLabel', group: 'channels' },
|
|
336
|
+
{ key: 'barkServerUrl', labelKey: 'settings.barkServerLabel', group: 'channels' },
|
|
337
|
+
{ key: 'barkKey', labelKey: 'settings.barkKeyLabel', group: 'channels' },
|
|
338
|
+
{ key: 'pushplusUrl', labelKey: 'settings.pushplusUrlLabel', group: 'channels' },
|
|
339
|
+
{ key: 'smtpHost', labelKey: 'settings.smtpHostLabel', group: 'channels' },
|
|
340
|
+
{ key: 'smtpPort', labelKey: 'settings.smtpPortLabel', group: 'channels' },
|
|
341
|
+
{ key: 'smtpUser', labelKey: 'settings.smtpUserLabel', group: 'channels' },
|
|
342
|
+
{ key: 'smtpFrom', labelKey: 'settings.smtpFromLabel', group: 'channels' },
|
|
343
|
+
{ key: 'smtpTo', labelKey: 'settings.smtpToLabel', group: 'channels' },
|
|
344
|
+
{ key: 'ttsBaseUrl', labelKey: 'settings.ttsBaseUrlLabel', group: 'channels' },
|
|
345
|
+
{ key: 'giteaBaseUrl', labelKey: 'settings.giteaBaseUrlLabel', group: 'channels' },
|
|
346
|
+
{ key: 'giteaRepo', labelKey: 'settings.giteaRepoLabel', group: 'channels' },
|
|
347
|
+
{ key: 'deliveryTimeoutMs', labelKey: 'settings.deliveryTimeoutMsLabel', group: 'channels', numeric: true },
|
|
348
|
+
];
|
|
349
|
+
const DELIVERY_TOGGLE_FIELDS = [
|
|
350
|
+
{ key: 'smtpSecure', labelKey: 'settings.smtpSecureLabel' },
|
|
351
|
+
];
|
|
352
|
+
const DELIVERY_SETTING_KEYS = DELIVERY_SETTING_FIELDS.map((f) => f.key).concat(DELIVERY_TOGGLE_FIELDS.map((f) => f.key));
|
|
353
|
+
// The global template plus the per-channel override map (server model).
|
|
354
|
+
const TEMPLATE_SETTING_KEYS = ['template', 'channelTemplates'];
|
|
355
|
+
|
|
356
|
+
/** Collect every delivery key the settings endpoints may return. */
|
|
357
|
+
function pickDeliverySettings(source) {
|
|
358
|
+
const out = {};
|
|
359
|
+
for (const key of DELIVERY_SETTING_KEYS.concat(TEMPLATE_SETTING_KEYS)) {
|
|
360
|
+
if (source && source[key] !== undefined && source[key] !== null) out[key] = source[key];
|
|
361
|
+
}
|
|
362
|
+
if (out.channelTemplates && typeof out.channelTemplates === 'object') {
|
|
363
|
+
out.channelTemplates = Object.assign({}, out.channelTemplates);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Shared delivery form used by both the task panel modal and the plugin
|
|
370
|
+
* settings card, so a new channel is added in exactly one place.
|
|
371
|
+
* Sections collapse via the canonical aria-expanded head contract.
|
|
372
|
+
*/
|
|
373
|
+
function renderDeliverySettings({ T, values, onChange, onChannelTemplate, sections, onToggleSection }) {
|
|
374
|
+
const val = (key) => (values[key] === undefined || values[key] === null ? '' : String(values[key]));
|
|
375
|
+
const field = (f) => React.createElement('div', { className: 'dsh-cron-form-group', key: f.key, style: { marginBottom: '10px' } },
|
|
376
|
+
React.createElement('label', null, T(f.labelKey)),
|
|
377
|
+
React.createElement('input', f.numeric ? {
|
|
378
|
+
type: 'number',
|
|
379
|
+
min: 1000,
|
|
380
|
+
step: 1000,
|
|
381
|
+
value: val(f.key),
|
|
382
|
+
placeholder: '15000',
|
|
383
|
+
onChange: (e) => onChange(f.key, e.target.value === '' ? undefined : Number(e.target.value)),
|
|
384
|
+
} : {
|
|
385
|
+
type: 'text',
|
|
386
|
+
value: val(f.key),
|
|
387
|
+
placeholder: f.placeholderKey ? T(f.placeholderKey) : '',
|
|
388
|
+
onChange: (e) => onChange(f.key, e.target.value)
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
const section = (key, titleKey, children) => {
|
|
392
|
+
const open = Boolean(sections[key]);
|
|
393
|
+
return React.createElement('div', { className: 'dsh-cron-section', key },
|
|
394
|
+
React.createElement('button', {
|
|
395
|
+
type: 'button',
|
|
396
|
+
className: 'dsh-cron-section-head',
|
|
397
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
398
|
+
onClick: () => onToggleSection(key)
|
|
399
|
+
},
|
|
400
|
+
React.createElement('span', null, T(titleKey)),
|
|
401
|
+
React.createElement('span', { className: 'dsh-cron-section-chevron', 'data-open': open ? 'true' : 'false' }, '▾')
|
|
402
|
+
),
|
|
403
|
+
open && React.createElement('div', { className: 'dsh-cron-section-body' }, children)
|
|
404
|
+
);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return [
|
|
408
|
+
section('secrets', 'settings.secretsSection', [
|
|
409
|
+
React.createElement('div', { className: 'dsh-cron-hint', key: 'hint', style: { marginBottom: '10px' } }, T('settings.secretsHint')),
|
|
410
|
+
...DELIVERY_SETTING_FIELDS.filter((f) => f.group === 'secrets').map(field),
|
|
411
|
+
]),
|
|
412
|
+
section('channels', 'settings.deliverySection', [
|
|
413
|
+
React.createElement('div', { className: 'dsh-cron-form-row', key: 'row' },
|
|
414
|
+
...DELIVERY_SETTING_FIELDS.filter((f) => f.group === 'channels').map(field)
|
|
415
|
+
),
|
|
416
|
+
...DELIVERY_TOGGLE_FIELDS.map((f) => React.createElement('label', { className: 'dsh-cron-checkbox-row', key: f.key, style: { marginBottom: '10px' } },
|
|
417
|
+
React.createElement('input', {
|
|
418
|
+
type: 'checkbox',
|
|
419
|
+
checked: Boolean(values[f.key]),
|
|
420
|
+
onChange: (e) => onChange(f.key, e.target.checked)
|
|
421
|
+
}),
|
|
422
|
+
React.createElement('span', null, T(f.labelKey))
|
|
423
|
+
)),
|
|
424
|
+
]),
|
|
425
|
+
section('templates', 'settings.templatesSection', [
|
|
426
|
+
React.createElement('div', { className: 'dsh-cron-form-group', key: 'global', style: { marginBottom: '10px' } },
|
|
427
|
+
React.createElement('label', null, T('settings.templateLabel')),
|
|
428
|
+
React.createElement('input', {
|
|
429
|
+
type: 'text',
|
|
430
|
+
value: val('template'),
|
|
431
|
+
onChange: (e) => onChange('template', e.target.value)
|
|
432
|
+
}),
|
|
433
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T('settings.templateHint'))
|
|
434
|
+
),
|
|
435
|
+
React.createElement('div', { className: 'dsh-cron-form-row', key: 'perchannel' },
|
|
436
|
+
...DELIVERY_CHANNELS.map((id) => React.createElement('div', { className: 'dsh-cron-form-group', key: id, style: { marginBottom: '10px' } },
|
|
437
|
+
React.createElement('label', null, T('settings.channelTemplateLabel', { channel: T(CHANNEL_LABEL_KEYS[id]) })),
|
|
438
|
+
React.createElement('input', {
|
|
439
|
+
type: 'text',
|
|
440
|
+
value: (values.channelTemplates && values.channelTemplates[id]) || '',
|
|
441
|
+
onChange: (e) => onChannelTemplate(id, e.target.value)
|
|
442
|
+
})
|
|
443
|
+
))
|
|
444
|
+
),
|
|
445
|
+
]),
|
|
446
|
+
];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Parse a KEY=VALUE textarea into the task env object (#38). */
|
|
450
|
+
function parseEnvText(text) {
|
|
451
|
+
const env = {};
|
|
452
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
453
|
+
const line = rawLine.trim();
|
|
454
|
+
if (!line || line.startsWith('#')) continue;
|
|
455
|
+
const idx = line.indexOf('=');
|
|
456
|
+
if (idx <= 0) continue;
|
|
457
|
+
const key = line.slice(0, idx).trim();
|
|
458
|
+
const value = line.slice(idx + 1).trim();
|
|
459
|
+
if (key) env[key] = value;
|
|
460
|
+
}
|
|
461
|
+
return Object.keys(env).length ? env : undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function formatEnvText(env) {
|
|
465
|
+
if (!env || typeof env !== 'object') return '';
|
|
466
|
+
return Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n');
|
|
467
|
+
}
|
|
468
|
+
|
|
234
469
|
// Status colors: core semantic state tokens first (same set dsh-clinebot
|
|
235
470
|
// uses), plugin fallbacks as a single point of change (#105).
|
|
236
471
|
const STYLES = `
|
|
@@ -316,6 +551,12 @@ window.__ModuleLoader__.load({
|
|
|
316
551
|
|
|
317
552
|
.dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
|
318
553
|
.dsh-cron-modal { background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 16px; width: 100%; max-width: 540px; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
|
|
554
|
+
.dsh-cron-modal-scroll { max-width: 620px; max-height: 88vh; overflow-y: auto; }
|
|
555
|
+
.dsh-cron-section-head { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 0; margin-bottom: 10px; font: inherit; font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); cursor: pointer; }
|
|
556
|
+
.dsh-cron-section-head:hover { color: var(--dsw-alias-label-primary, #eee); }
|
|
557
|
+
.dsh-cron-section-chevron { transition: transform 0.15s ease; display: inline-block; }
|
|
558
|
+
.dsh-cron-section-chevron[data-open="false"] { transform: rotate(-90deg); }
|
|
559
|
+
.dsh-cron-section-body { padding-bottom: 4px; }
|
|
319
560
|
.dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
|
|
320
561
|
.dsh-cron-form-group { margin-bottom: 16px; }
|
|
321
562
|
.dsh-cron-form-group label { display: block; font-size: 13px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 6px; }
|
|
@@ -373,6 +614,12 @@ window.__ModuleLoader__.load({
|
|
|
373
614
|
.dsh-cron-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--dsw-alias-label-secondary, #ccc); cursor: pointer; user-select: none; }
|
|
374
615
|
.dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary, #ededed); }
|
|
375
616
|
.dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); margin-top: 4px; }
|
|
617
|
+
.dsh-cron-channel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px 12px; }
|
|
618
|
+
.dsh-cron-section { border-top: 1px solid var(--dsw-alias-border-l2, #2e2e2e); margin-top: 16px; padding-top: 12px; }
|
|
619
|
+
.dsh-cron-section-title { font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 10px; }
|
|
620
|
+
.dsh-cron-secret-row { display: flex; align-items: center; gap: 8px; }
|
|
621
|
+
.dsh-cron-secret-row input { flex: 1; }
|
|
622
|
+
.dsh-cron-badge-ref { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-3, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-tertiary, #888); white-space: nowrap; }
|
|
376
623
|
.dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
|
|
377
624
|
.dsh-cron-status-banner[data-type="success"] { background: var(--dsh-cron-success-bg); border: 1px solid var(--dsh-cron-success); color: var(--dsh-cron-success); }
|
|
378
625
|
.dsh-cron-status-banner[data-type="error"] { background: var(--dsh-cron-danger-bg); border: 1px solid var(--dsh-cron-danger); color: var(--dsh-cron-danger); }
|
|
@@ -496,6 +743,8 @@ window.__ModuleLoader__.load({
|
|
|
496
743
|
const [taskHistory, setTaskHistory] = React.useState([]);
|
|
497
744
|
const [historyLoading, setHistoryLoading] = React.useState(false);
|
|
498
745
|
const [formNotifyTelegram, setFormNotifyTelegram] = React.useState(false);
|
|
746
|
+
const [formChannels, setFormChannels] = React.useState([]);
|
|
747
|
+
const [formTemplate, setFormTemplate] = React.useState('');
|
|
499
748
|
const [formOnlyOnFailure, setFormOnlyOnFailure] = React.useState(false);
|
|
500
749
|
const [formTimeoutSeconds, setFormTimeoutSeconds] = React.useState(1800);
|
|
501
750
|
const [formOverlapPolicy, setFormOverlapPolicy] = React.useState('skip');
|
|
@@ -504,6 +753,23 @@ window.__ModuleLoader__.load({
|
|
|
504
753
|
const [formMisfirePolicy, setFormMisfirePolicy] = React.useState('skip');
|
|
505
754
|
const [formMaxRetries, setFormMaxRetries] = React.useState(0);
|
|
506
755
|
const [formPermissionPreset, setFormPermissionPreset] = React.useState('default');
|
|
756
|
+
const [formEnv, setFormEnv] = React.useState('');
|
|
757
|
+
const [formCwd, setFormCwd] = React.useState('');
|
|
758
|
+
const [formWorkspaceId, setFormWorkspaceId] = React.useState('');
|
|
759
|
+
const [formWorktree, setFormWorktree] = React.useState(false);
|
|
760
|
+
const [formKeepWorktree, setFormKeepWorktree] = React.useState(false);
|
|
761
|
+
const [formHttpMethod, setFormHttpMethod] = React.useState('GET');
|
|
762
|
+
const [formHttpUrl, setFormHttpUrl] = React.useState('');
|
|
763
|
+
const [formHttpHeaders, setFormHttpHeaders] = React.useState('');
|
|
764
|
+
const [formHttpBody, setFormHttpBody] = React.useState('');
|
|
765
|
+
const [formSshProfileId, setFormSshProfileId] = React.useState('');
|
|
766
|
+
const [formSshTarget, setFormSshTarget] = React.useState('');
|
|
767
|
+
const [formSshKeyPath, setFormSshKeyPath] = React.useState('');
|
|
768
|
+
const [formDockerImage, setFormDockerImage] = React.useState('');
|
|
769
|
+
const [formNodePath, setFormNodePath] = React.useState('');
|
|
770
|
+
const [formPythonPath, setFormPythonPath] = React.useState('');
|
|
771
|
+
const [formSkillName, setFormSkillName] = React.useState('');
|
|
772
|
+
const [formWorkflowName, setFormWorkflowName] = React.useState('');
|
|
507
773
|
const [stats, setStats] = React.useState(null);
|
|
508
774
|
|
|
509
775
|
// Live execution indicator (#35): a light 1s tick only while some task
|
|
@@ -527,6 +793,8 @@ window.__ModuleLoader__.load({
|
|
|
527
793
|
const [settingsNotifyGlobal, setSettingsNotifyGlobal] = React.useState(false);
|
|
528
794
|
const [settingsOnlyOnFailureGlobal, setSettingsOnlyOnFailureGlobal] = React.useState(false);
|
|
529
795
|
const [settingsKanbanUrl, setSettingsKanbanUrl] = React.useState('http://127.0.0.1:3000');
|
|
796
|
+
const [settingsDelivery, setSettingsDelivery] = React.useState({});
|
|
797
|
+
const [settingsSections, setSettingsSections] = React.useState({ channels: true });
|
|
530
798
|
const [settingsHasDefault, setSettingsHasDefault] = React.useState(false);
|
|
531
799
|
const [settingsHasCustomBotToken, setSettingsHasCustomBotToken] = React.useState(false);
|
|
532
800
|
const [testStatus, setTestStatus] = React.useState(null);
|
|
@@ -654,6 +922,7 @@ window.__ModuleLoader__.load({
|
|
|
654
922
|
setSettingsNotifyGlobal(Boolean(data.settings.notifyTelegram));
|
|
655
923
|
setSettingsOnlyOnFailureGlobal(Boolean(data.settings.onlyOnFailure));
|
|
656
924
|
setSettingsKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
925
|
+
setSettingsDelivery(pickDeliverySettings(data.settings));
|
|
657
926
|
setSettingsHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
658
927
|
setSettingsHasCustomBotToken(Boolean(data.settings.hasCustomBotToken));
|
|
659
928
|
}
|
|
@@ -666,13 +935,13 @@ window.__ModuleLoader__.load({
|
|
|
666
935
|
const res = await fetch('/dsh-cron/settings', {
|
|
667
936
|
method: 'POST',
|
|
668
937
|
headers: { 'Content-Type': 'application/json' },
|
|
669
|
-
body: JSON.stringify({
|
|
938
|
+
body: JSON.stringify(Object.assign({
|
|
670
939
|
botToken: settingsBotToken,
|
|
671
940
|
chatId: settingsChatId,
|
|
672
941
|
notifyTelegram: settingsNotifyGlobal,
|
|
673
942
|
onlyOnFailure: settingsOnlyOnFailureGlobal,
|
|
674
943
|
kanbanBaseUrl: settingsKanbanUrl,
|
|
675
|
-
})
|
|
944
|
+
}, settingsDelivery))
|
|
676
945
|
});
|
|
677
946
|
const data = await res.json();
|
|
678
947
|
if (data && data.ok) {
|
|
@@ -685,8 +954,35 @@ window.__ModuleLoader__.load({
|
|
|
685
954
|
}
|
|
686
955
|
};
|
|
687
956
|
|
|
688
|
-
|
|
689
|
-
|
|
957
|
+
/**
|
|
958
|
+
* Update one delivery setting without dropping the rest of the object.
|
|
959
|
+
* `undefined` removes the key so the server default applies again; an
|
|
960
|
+
* empty string is kept so a text field can be cleared on purpose.
|
|
961
|
+
*/
|
|
962
|
+
const updateDelivery = (key, value) => {
|
|
963
|
+
setSettingsDelivery((prev) => {
|
|
964
|
+
const next = Object.assign({}, prev);
|
|
965
|
+
if (value === undefined) delete next[key];
|
|
966
|
+
else next[key] = value;
|
|
967
|
+
return next;
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
/** Per-channel template overrides live in a nested map on the server. */
|
|
972
|
+
const updateChannelTemplate = (channelId, value) => {
|
|
973
|
+
setSettingsDelivery((prev) => {
|
|
974
|
+
const templates = Object.assign({}, prev.channelTemplates || {});
|
|
975
|
+
if (value && value.trim()) templates[channelId] = value;
|
|
976
|
+
else delete templates[channelId];
|
|
977
|
+
return Object.assign({}, prev, { channelTemplates: templates });
|
|
978
|
+
});
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const toggleDeliverySection = (key) => {
|
|
982
|
+
setSettingsSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }));
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const handleTestTelegram = async () => { setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
|
|
690
986
|
try {
|
|
691
987
|
const res = await fetch('/dsh-cron/telegram/test', {
|
|
692
988
|
method: 'POST',
|
|
@@ -714,6 +1010,8 @@ window.__ModuleLoader__.load({
|
|
|
714
1010
|
setFormType('llm');
|
|
715
1011
|
setFormDelivery('isolated');
|
|
716
1012
|
setFormNotifyTelegram(false);
|
|
1013
|
+
setFormChannels([]);
|
|
1014
|
+
setFormTemplate('');
|
|
717
1015
|
setFormOnlyOnFailure(false);
|
|
718
1016
|
setFormTimeoutSeconds(1800);
|
|
719
1017
|
setFormOverlapPolicy('skip');
|
|
@@ -722,6 +1020,23 @@ window.__ModuleLoader__.load({
|
|
|
722
1020
|
setFormMisfirePolicy('skip');
|
|
723
1021
|
setFormMaxRetries(0);
|
|
724
1022
|
setFormPermissionPreset('default');
|
|
1023
|
+
setFormEnv('');
|
|
1024
|
+
setFormCwd('');
|
|
1025
|
+
setFormWorkspaceId('');
|
|
1026
|
+
setFormWorktree(false);
|
|
1027
|
+
setFormKeepWorktree(false);
|
|
1028
|
+
setFormHttpMethod('GET');
|
|
1029
|
+
setFormHttpUrl('');
|
|
1030
|
+
setFormHttpHeaders('');
|
|
1031
|
+
setFormHttpBody('');
|
|
1032
|
+
setFormSshProfileId('');
|
|
1033
|
+
setFormSshTarget('');
|
|
1034
|
+
setFormSshKeyPath('');
|
|
1035
|
+
setFormDockerImage('');
|
|
1036
|
+
setFormNodePath('');
|
|
1037
|
+
setFormPythonPath('');
|
|
1038
|
+
setFormSkillName('');
|
|
1039
|
+
setFormWorkflowName('');
|
|
725
1040
|
if (preset) {
|
|
726
1041
|
setFormTitle(preset.title || '');
|
|
727
1042
|
setFormSchedule(preset.schedule || '');
|
|
@@ -768,6 +1083,8 @@ window.__ModuleLoader__.load({
|
|
|
768
1083
|
setFormModel(task.model || '');
|
|
769
1084
|
setFormDelivery(task.delivery || 'isolated');
|
|
770
1085
|
setFormNotifyTelegram(Boolean(task.notifyTelegram));
|
|
1086
|
+
setFormChannels(Array.isArray(task.channels) ? task.channels.filter((c) => DELIVERY_CHANNELS.includes(c)) : []);
|
|
1087
|
+
setFormTemplate(task.template || '');
|
|
771
1088
|
setFormOnlyOnFailure(Boolean(task.onlyOnFailure));
|
|
772
1089
|
setFormTimeoutSeconds(task.timeoutSeconds !== undefined ? task.timeoutSeconds : 1800);
|
|
773
1090
|
setFormOverlapPolicy(task.overlapPolicy || 'skip');
|
|
@@ -776,6 +1093,23 @@ window.__ModuleLoader__.load({
|
|
|
776
1093
|
setFormMisfirePolicy(task.misfirePolicy || 'skip');
|
|
777
1094
|
setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
|
|
778
1095
|
setFormPermissionPreset(task.permissionPreset || 'default');
|
|
1096
|
+
setFormEnv(formatEnvText(task.env));
|
|
1097
|
+
setFormCwd(task.cwd || '');
|
|
1098
|
+
setFormWorkspaceId(task.workspaceId || '');
|
|
1099
|
+
setFormWorktree(Boolean(task.worktree));
|
|
1100
|
+
setFormKeepWorktree(Boolean(task.keepWorktree));
|
|
1101
|
+
setFormHttpMethod(task.httpMethod || 'GET');
|
|
1102
|
+
setFormHttpUrl(task.httpUrl || '');
|
|
1103
|
+
setFormHttpHeaders(typeof task.httpHeaders === 'string' ? task.httpHeaders : (task.httpHeaders ? JSON.stringify(task.httpHeaders) : ''));
|
|
1104
|
+
setFormHttpBody(task.httpBody || '');
|
|
1105
|
+
setFormSshProfileId(task.sshProfileId || '');
|
|
1106
|
+
setFormSshTarget(task.sshTarget || '');
|
|
1107
|
+
setFormSshKeyPath(task.sshKeyPath || '');
|
|
1108
|
+
setFormDockerImage(task.dockerImage || '');
|
|
1109
|
+
setFormNodePath(task.nodePath || '');
|
|
1110
|
+
setFormPythonPath(task.pythonPath || '');
|
|
1111
|
+
setFormSkillName(task.skillName || '');
|
|
1112
|
+
setFormWorkflowName(task.workflowName || '');
|
|
779
1113
|
if (task.provider) loadModels(task.provider);
|
|
780
1114
|
else if (defaultModelInfo?.provider) loadModels(defaultModelInfo.provider);
|
|
781
1115
|
loadTaskHistory(task.id);
|
|
@@ -792,17 +1126,20 @@ window.__ModuleLoader__.load({
|
|
|
792
1126
|
const handleSaveManual = async (e) => {
|
|
793
1127
|
e.preventDefault();
|
|
794
1128
|
try {
|
|
1129
|
+
const isAgentType = AGENT_FORM_TYPES.includes(formType);
|
|
795
1130
|
const payload = {
|
|
796
1131
|
id: formId || undefined,
|
|
797
1132
|
status: selectedTaskMeta ? selectedTaskMeta.status : 'active',
|
|
798
1133
|
title: formTitle,
|
|
799
1134
|
schedule: formSchedule,
|
|
800
|
-
prompt: formPrompt,
|
|
1135
|
+
prompt: formType === 'http' ? (formPrompt.trim() || formHttpUrl.trim()) : formPrompt,
|
|
801
1136
|
type: formType,
|
|
802
1137
|
delivery: formDelivery || 'isolated',
|
|
803
|
-
provider:
|
|
804
|
-
model:
|
|
1138
|
+
provider: isAgentType ? (formProvider || undefined) : undefined,
|
|
1139
|
+
model: isAgentType ? (formModel || undefined) : undefined,
|
|
805
1140
|
notifyTelegram: formNotifyTelegram,
|
|
1141
|
+
channels: formChannels,
|
|
1142
|
+
template: formTemplate.trim(),
|
|
806
1143
|
onlyOnFailure: formOnlyOnFailure,
|
|
807
1144
|
timeoutSeconds: Number(formTimeoutSeconds) || 1800,
|
|
808
1145
|
overlapPolicy: formOverlapPolicy,
|
|
@@ -811,11 +1148,28 @@ window.__ModuleLoader__.load({
|
|
|
811
1148
|
maxRetries: Number(formMaxRetries) || 0,
|
|
812
1149
|
permissionPreset: formPermissionPreset,
|
|
813
1150
|
kanbanMode: formKanbanMode,
|
|
1151
|
+
env: parseEnvText(formEnv),
|
|
1152
|
+
cwd: formCwd.trim(),
|
|
1153
|
+
workspaceId: formWorkspaceId.trim(),
|
|
1154
|
+
worktree: isAgentType ? Boolean(formWorktree) : false,
|
|
1155
|
+
keepWorktree: isAgentType ? Boolean(formKeepWorktree) : false,
|
|
1156
|
+
httpMethod: formHttpMethod,
|
|
1157
|
+
httpUrl: formHttpUrl.trim(),
|
|
1158
|
+
httpHeaders: formHttpHeaders.trim() || undefined,
|
|
1159
|
+
httpBody: formHttpBody,
|
|
1160
|
+
sshProfileId: formSshProfileId.trim(),
|
|
1161
|
+
sshTarget: formSshTarget.trim(),
|
|
1162
|
+
sshKeyPath: formSshKeyPath.trim(),
|
|
1163
|
+
dockerImage: formDockerImage.trim(),
|
|
1164
|
+
nodePath: formNodePath.trim(),
|
|
1165
|
+
pythonPath: formPythonPath.trim(),
|
|
1166
|
+
skillName: formSkillName.trim(),
|
|
1167
|
+
workflowName: formWorkflowName.trim(),
|
|
814
1168
|
};
|
|
815
|
-
// Cross-origin hardening:
|
|
816
|
-
// header, which
|
|
1169
|
+
// Cross-origin hardening: every code-executing type needs the explicit
|
|
1170
|
+
// confirm header, which forged cross-site posts cannot attach (#86)
|
|
817
1171
|
const headers = { 'Content-Type': 'application/json' };
|
|
818
|
-
if (formType
|
|
1172
|
+
if (CODE_FORM_TYPES.includes(formType)) headers[SCRIPT_CONFIRM_HEADER] = 'script';
|
|
819
1173
|
const res = await fetch('/dsh-cron/tasks', {
|
|
820
1174
|
method: 'POST',
|
|
821
1175
|
headers,
|
|
@@ -1176,7 +1530,14 @@ window.__ModuleLoader__.load({
|
|
|
1176
1530
|
onChange: (e) => setFormType(e.target.value)
|
|
1177
1531
|
},
|
|
1178
1532
|
React.createElement('option', { value: 'llm' }, T_KEY(t, 'form.typeLlm')),
|
|
1179
|
-
React.createElement('option', { value: 'script' }, T_KEY(t, 'form.typeScript'))
|
|
1533
|
+
React.createElement('option', { value: 'script' }, T_KEY(t, 'form.typeScript')),
|
|
1534
|
+
React.createElement('option', { value: 'node' }, T_KEY(t, 'form.typeNode')),
|
|
1535
|
+
React.createElement('option', { value: 'python' }, T_KEY(t, 'form.typePython')),
|
|
1536
|
+
React.createElement('option', { value: 'http' }, T_KEY(t, 'form.typeHttp')),
|
|
1537
|
+
React.createElement('option', { value: 'ssh' }, T_KEY(t, 'form.typeSsh')),
|
|
1538
|
+
React.createElement('option', { value: 'docker' }, T_KEY(t, 'form.typeDocker')),
|
|
1539
|
+
React.createElement('option', { value: 'skill' }, T_KEY(t, 'form.typeSkill')),
|
|
1540
|
+
React.createElement('option', { value: 'workflow' }, T_KEY(t, 'form.typeWorkflow'))
|
|
1180
1541
|
)
|
|
1181
1542
|
),
|
|
1182
1543
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
@@ -1268,7 +1629,7 @@ window.__ModuleLoader__.load({
|
|
|
1268
1629
|
React.createElement('option', { value: 'always' }, T_KEY(t, 'form.kanbanAlways'))
|
|
1269
1630
|
)
|
|
1270
1631
|
),
|
|
1271
|
-
formType
|
|
1632
|
+
AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1272
1633
|
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1273
1634
|
React.createElement('label', null, T_KEY(t, 'form.providerLabel')),
|
|
1274
1635
|
React.createElement('select', {
|
|
@@ -1290,14 +1651,165 @@ window.__ModuleLoader__.load({
|
|
|
1290
1651
|
)
|
|
1291
1652
|
)
|
|
1292
1653
|
),
|
|
1293
|
-
React.createElement('div', { className: 'dsh-cron-form-
|
|
1294
|
-
React.createElement('
|
|
1654
|
+
formType === 'http' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1655
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1656
|
+
React.createElement('label', null, T_KEY(t, 'form.httpMethodLabel')),
|
|
1657
|
+
React.createElement('select', {
|
|
1658
|
+
value: formHttpMethod,
|
|
1659
|
+
onChange: (e) => setFormHttpMethod(e.target.value)
|
|
1660
|
+
},
|
|
1661
|
+
['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].map((m) => React.createElement('option', { key: m, value: m }, m))
|
|
1662
|
+
)
|
|
1663
|
+
),
|
|
1664
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1665
|
+
React.createElement('label', null, T_KEY(t, 'form.httpUrlLabel')),
|
|
1666
|
+
React.createElement('input', {
|
|
1667
|
+
required: true,
|
|
1668
|
+
value: formHttpUrl,
|
|
1669
|
+
placeholder: 'https://example.test/webhook',
|
|
1670
|
+
onChange: (e) => setFormHttpUrl(e.target.value)
|
|
1671
|
+
})
|
|
1672
|
+
)
|
|
1673
|
+
),
|
|
1674
|
+
formType === 'http' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1675
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1676
|
+
React.createElement('label', null, T_KEY(t, 'form.httpHeadersLabel')),
|
|
1677
|
+
React.createElement('input', {
|
|
1678
|
+
value: formHttpHeaders,
|
|
1679
|
+
placeholder: '{"Authorization":"Bearer ..."}',
|
|
1680
|
+
onChange: (e) => setFormHttpHeaders(e.target.value)
|
|
1681
|
+
})
|
|
1682
|
+
),
|
|
1683
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1684
|
+
React.createElement('label', null, T_KEY(t, 'form.httpBodyLabel')),
|
|
1685
|
+
React.createElement('input', {
|
|
1686
|
+
value: formHttpBody,
|
|
1687
|
+
onChange: (e) => setFormHttpBody(e.target.value)
|
|
1688
|
+
})
|
|
1689
|
+
)
|
|
1690
|
+
),
|
|
1691
|
+
formType === 'ssh' && React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1692
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1693
|
+
React.createElement('label', null, T_KEY(t, 'form.sshProfileLabel')),
|
|
1694
|
+
React.createElement('input', {
|
|
1695
|
+
value: formSshProfileId,
|
|
1696
|
+
placeholder: 'prod-web',
|
|
1697
|
+
onChange: (e) => setFormSshProfileId(e.target.value)
|
|
1698
|
+
}),
|
|
1699
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.sshProfileHint'))
|
|
1700
|
+
),
|
|
1701
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1702
|
+
React.createElement('label', null, T_KEY(t, 'form.sshTargetLabel')),
|
|
1703
|
+
React.createElement('input', {
|
|
1704
|
+
value: formSshTarget,
|
|
1705
|
+
placeholder: 'root@10.0.0.5',
|
|
1706
|
+
onChange: (e) => setFormSshTarget(e.target.value)
|
|
1707
|
+
}),
|
|
1708
|
+
React.createElement('input', {
|
|
1709
|
+
style: { marginTop: '6px' },
|
|
1710
|
+
value: formSshKeyPath,
|
|
1711
|
+
placeholder: '~/.ssh/id_ed25519',
|
|
1712
|
+
onChange: (e) => setFormSshKeyPath(e.target.value)
|
|
1713
|
+
})
|
|
1714
|
+
)
|
|
1715
|
+
),
|
|
1716
|
+
formType === 'docker' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1717
|
+
React.createElement('label', null, T_KEY(t, 'form.dockerImageLabel')),
|
|
1718
|
+
React.createElement('input', {
|
|
1719
|
+
required: true,
|
|
1720
|
+
value: formDockerImage,
|
|
1721
|
+
placeholder: 'alpine:3.20',
|
|
1722
|
+
onChange: (e) => setFormDockerImage(e.target.value)
|
|
1723
|
+
})
|
|
1724
|
+
),
|
|
1725
|
+
formType === 'skill' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1726
|
+
React.createElement('label', null, T_KEY(t, 'form.skillNameLabel')),
|
|
1727
|
+
React.createElement('input', {
|
|
1728
|
+
value: formSkillName,
|
|
1729
|
+
placeholder: 'gitea',
|
|
1730
|
+
onChange: (e) => setFormSkillName(e.target.value)
|
|
1731
|
+
})
|
|
1732
|
+
),
|
|
1733
|
+
formType === 'workflow' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1734
|
+
React.createElement('label', null, T_KEY(t, 'form.workflowNameLabel')),
|
|
1735
|
+
React.createElement('input', {
|
|
1736
|
+
value: formWorkflowName,
|
|
1737
|
+
placeholder: 'nightly-release',
|
|
1738
|
+
onChange: (e) => setFormWorkflowName(e.target.value)
|
|
1739
|
+
})
|
|
1740
|
+
),
|
|
1741
|
+
(formType === 'node' || formType === 'python') && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1742
|
+
React.createElement('label', null, formType === 'node' ? 'Node.js binary (optional)' : 'Python interpreter (optional)'),
|
|
1743
|
+
React.createElement('input', {
|
|
1744
|
+
value: formType === 'node' ? formNodePath : formPythonPath,
|
|
1745
|
+
placeholder: formType === 'node' ? 'auto' : '.venv/bin/python',
|
|
1746
|
+
onChange: (e) => (formType === 'node' ? setFormNodePath(e.target.value) : setFormPythonPath(e.target.value))
|
|
1747
|
+
})
|
|
1748
|
+
),
|
|
1749
|
+
!AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1750
|
+
React.createElement('label', null, T_KEY(t, 'form.envLabel')),
|
|
1751
|
+
React.createElement('textarea', {
|
|
1752
|
+
rows: 3,
|
|
1753
|
+
style: { fontFamily: 'monospace', fontSize: '12.5px' },
|
|
1754
|
+
value: formEnv,
|
|
1755
|
+
placeholder: T_KEY(t, 'form.envPlaceholder'),
|
|
1756
|
+
onChange: (e) => setFormEnv(e.target.value)
|
|
1757
|
+
}),
|
|
1758
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.envHint'))
|
|
1759
|
+
),
|
|
1760
|
+
React.createElement('div', { className: 'dsh-cron-form-row' },
|
|
1761
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1762
|
+
React.createElement('label', null, T_KEY(t, 'form.cwdLabel')),
|
|
1763
|
+
React.createElement('input', {
|
|
1764
|
+
value: formCwd,
|
|
1765
|
+
placeholder: '/srv/app',
|
|
1766
|
+
onChange: (e) => setFormCwd(e.target.value)
|
|
1767
|
+
})
|
|
1768
|
+
),
|
|
1769
|
+
React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1770
|
+
React.createElement('label', null, T_KEY(t, 'form.workspaceLabel')),
|
|
1771
|
+
React.createElement('input', {
|
|
1772
|
+
value: formWorkspaceId,
|
|
1773
|
+
onChange: (e) => setFormWorkspaceId(e.target.value)
|
|
1774
|
+
})
|
|
1775
|
+
)
|
|
1776
|
+
),
|
|
1777
|
+
AGENT_FORM_TYPES.includes(formType) && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1778
|
+
React.createElement('label', { className: 'dsh-cron-checkbox-row' },
|
|
1779
|
+
React.createElement('input', {
|
|
1780
|
+
type: 'checkbox',
|
|
1781
|
+
checked: formWorktree,
|
|
1782
|
+
onChange: (e) => setFormWorktree(e.target.checked)
|
|
1783
|
+
}),
|
|
1784
|
+
React.createElement('span', null, T_KEY(t, 'form.worktreeLabel'))
|
|
1785
|
+
),
|
|
1786
|
+
formWorktree && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
|
|
1787
|
+
React.createElement('input', {
|
|
1788
|
+
type: 'checkbox',
|
|
1789
|
+
checked: formKeepWorktree,
|
|
1790
|
+
onChange: (e) => setFormKeepWorktree(e.target.checked)
|
|
1791
|
+
}),
|
|
1792
|
+
React.createElement('span', null, T_KEY(t, 'form.keepWorktreeLabel'))
|
|
1793
|
+
)
|
|
1794
|
+
),
|
|
1795
|
+
formType !== 'http' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
1796
|
+
React.createElement('label', null, AGENT_FORM_TYPES.includes(formType) ? T_KEY(t, 'form.promptLabelLlm') : T_KEY(t, 'form.promptLabelScript')),
|
|
1295
1797
|
React.createElement('textarea', {
|
|
1296
1798
|
required: true,
|
|
1297
1799
|
rows: 9,
|
|
1298
1800
|
style: { fontFamily: 'monospace', fontSize: '12.5px', lineHeight: '1.45' },
|
|
1299
1801
|
value: formPrompt,
|
|
1300
|
-
placeholder:
|
|
1802
|
+
placeholder: AGENT_FORM_TYPES.includes(formType)
|
|
1803
|
+
? T_KEY(t, 'form.promptPlaceholderLlm')
|
|
1804
|
+
: formType === 'docker'
|
|
1805
|
+
? 'python -V'
|
|
1806
|
+
: formType === 'ssh'
|
|
1807
|
+
? 'uname -a'
|
|
1808
|
+
: formType === 'node'
|
|
1809
|
+
? 'console.log("hello")'
|
|
1810
|
+
: formType === 'python'
|
|
1811
|
+
? 'print("hello")'
|
|
1812
|
+
: T_KEY(t, 'form.promptPlaceholderScript'),
|
|
1301
1813
|
onChange: (e) => setFormPrompt(e.target.value)
|
|
1302
1814
|
})
|
|
1303
1815
|
),
|
|
@@ -1322,13 +1834,43 @@ window.__ModuleLoader__.load({
|
|
|
1322
1834
|
}),
|
|
1323
1835
|
React.createElement('span', null, T_KEY(t, 'form.notifyTelegram'))
|
|
1324
1836
|
),
|
|
1325
|
-
formNotifyTelegram && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
|
|
1837
|
+
(formNotifyTelegram || formChannels.length > 0) && React.createElement('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: '24px', marginTop: '6px' } },
|
|
1326
1838
|
React.createElement('input', {
|
|
1327
1839
|
type: 'checkbox',
|
|
1328
1840
|
checked: formOnlyOnFailure,
|
|
1329
1841
|
onChange: (e) => setFormOnlyOnFailure(e.target.checked)
|
|
1330
1842
|
}),
|
|
1331
1843
|
React.createElement('span', null, T_KEY(t, 'form.notifyOnlyFailure'))
|
|
1844
|
+
),
|
|
1845
|
+
React.createElement('div', { style: { marginTop: '12px', paddingTop: '10px', borderTop: '1px solid var(--dsw-alias-border-l2, #2e2e2e)' } },
|
|
1846
|
+
React.createElement('div', { style: { fontWeight: 500, fontSize: '12.5px', color: 'var(--dsw-alias-label-secondary, #aaa)', marginBottom: '8px' } }, T_KEY(t, 'form.channelsTitle')),
|
|
1847
|
+
React.createElement('div', { className: 'dsh-cron-channel-grid' },
|
|
1848
|
+
...DELIVERY_CHANNELS.map((id) => React.createElement('label', { key: id, className: 'dsh-cron-checkbox-row' },
|
|
1849
|
+
React.createElement('input', {
|
|
1850
|
+
type: 'checkbox',
|
|
1851
|
+
checked: formChannels.indexOf(id) >= 0,
|
|
1852
|
+
onChange: (e) => {
|
|
1853
|
+
setFormChannels((prev) => {
|
|
1854
|
+
const set = new Set(prev);
|
|
1855
|
+
if (e.target.checked) set.add(id); else set.delete(id);
|
|
1856
|
+
return DELIVERY_CHANNELS.filter((c) => set.has(c));
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
}),
|
|
1860
|
+
React.createElement('span', null, T_KEY(t, CHANNEL_LABEL_KEYS[id]))
|
|
1861
|
+
))
|
|
1862
|
+
),
|
|
1863
|
+
React.createElement('div', { className: 'dsh-cron-hint', style: { marginTop: '6px' } }, T_KEY(t, 'form.channelsHint'))
|
|
1864
|
+
),
|
|
1865
|
+
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginTop: '12px', marginBottom: 0 } },
|
|
1866
|
+
React.createElement('label', null, T_KEY(t, 'form.templateLabel')),
|
|
1867
|
+
React.createElement('input', {
|
|
1868
|
+
type: 'text',
|
|
1869
|
+
value: formTemplate,
|
|
1870
|
+
placeholder: T_KEY(t, 'form.templatePlaceholder'),
|
|
1871
|
+
onChange: (e) => setFormTemplate(e.target.value)
|
|
1872
|
+
}),
|
|
1873
|
+
React.createElement('div', { className: 'dsh-cron-hint' }, T_KEY(t, 'form.templateHint'))
|
|
1332
1874
|
)
|
|
1333
1875
|
),
|
|
1334
1876
|
selectedTaskMeta && React.createElement('div', {
|
|
@@ -1376,7 +1918,7 @@ window.__ModuleLoader__.load({
|
|
|
1376
1918
|
|
|
1377
1919
|
// 2. Settings modal
|
|
1378
1920
|
settingsModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setSettingsModalOpen(false) },
|
|
1379
|
-
React.createElement('div', { className: 'dsh-cron-modal', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
1921
|
+
React.createElement('div', { className: 'dsh-cron-modal dsh-cron-modal-scroll', role: 'dialog', 'aria-modal': 'true', onClick: (e) => e.stopPropagation() },
|
|
1380
1922
|
React.createElement('div', { className: 'dsh-cron-modal-head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '18px' } },
|
|
1381
1923
|
React.createElement('div', { className: 'dsh-cron-modal-title', style: { margin: 0, display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1382
1924
|
React.createElement('span', { style: { display: 'flex', alignItems: 'center', color: 'var(--dsh-cron-info)' }, ...iconHtml(ICON_TELEGRAM) }),
|
|
@@ -1425,6 +1967,14 @@ window.__ModuleLoader__.load({
|
|
|
1425
1967
|
React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
|
|
1426
1968
|
)
|
|
1427
1969
|
),
|
|
1970
|
+
...renderDeliverySettings({
|
|
1971
|
+
T: (key, vars) => T_KEY(t, key, vars),
|
|
1972
|
+
values: settingsDelivery,
|
|
1973
|
+
onChange: updateDelivery,
|
|
1974
|
+
onChannelTemplate: updateChannelTemplate,
|
|
1975
|
+
sections: settingsSections,
|
|
1976
|
+
onToggleSection: toggleDeliverySection,
|
|
1977
|
+
}),
|
|
1428
1978
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '14px' } },
|
|
1429
1979
|
React.createElement('button', {
|
|
1430
1980
|
type: 'button',
|
|
@@ -1510,6 +2060,8 @@ window.__ModuleLoader__.load({
|
|
|
1510
2060
|
const [notify, setNotify] = React.useState(false);
|
|
1511
2061
|
const [onlyFailure, setOnlyFailure] = React.useState(false);
|
|
1512
2062
|
const [kanbanUrl, setKanbanUrl] = React.useState('http://127.0.0.1:3000');
|
|
2063
|
+
const [delivery, setDelivery] = React.useState({});
|
|
2064
|
+
const [sections, setSections] = React.useState({ channels: true });
|
|
1513
2065
|
const [hasDefault, setHasDefault] = React.useState(false);
|
|
1514
2066
|
const [saved, setSaved] = React.useState(false);
|
|
1515
2067
|
const [testInfo, setTestInfo] = React.useState(null);
|
|
@@ -1534,6 +2086,7 @@ window.__ModuleLoader__.load({
|
|
|
1534
2086
|
setNotify(Boolean(data.settings.notifyTelegram));
|
|
1535
2087
|
setOnlyFailure(Boolean(data.settings.onlyOnFailure));
|
|
1536
2088
|
setKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
2089
|
+
setDelivery(pickDeliverySettings(data.settings));
|
|
1537
2090
|
setHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
1538
2091
|
setLoadState('ready');
|
|
1539
2092
|
} else {
|
|
@@ -1550,13 +2103,13 @@ window.__ModuleLoader__.load({
|
|
|
1550
2103
|
const res = await fetch('/dsh-cron/settings', {
|
|
1551
2104
|
method: 'POST',
|
|
1552
2105
|
headers: { 'Content-Type': 'application/json' },
|
|
1553
|
-
body: JSON.stringify({
|
|
2106
|
+
body: JSON.stringify(Object.assign({
|
|
1554
2107
|
botToken: token,
|
|
1555
2108
|
chatId,
|
|
1556
2109
|
notifyTelegram: notify,
|
|
1557
2110
|
onlyOnFailure: onlyFailure,
|
|
1558
2111
|
kanbanBaseUrl: kanbanUrl
|
|
1559
|
-
})
|
|
2112
|
+
}, delivery))
|
|
1560
2113
|
});
|
|
1561
2114
|
const data = await res.json();
|
|
1562
2115
|
if (data && data.ok) {
|
|
@@ -1686,6 +2239,24 @@ window.__ModuleLoader__.load({
|
|
|
1686
2239
|
React.createElement('span', null, T_KEY(t, 'settings.globalOnlyFailure'))
|
|
1687
2240
|
)
|
|
1688
2241
|
),
|
|
2242
|
+
...renderDeliverySettings({
|
|
2243
|
+
T: (key, vars) => T_KEY(t, key, vars),
|
|
2244
|
+
values: delivery,
|
|
2245
|
+
onChange: (key, value) => setDelivery((prev) => {
|
|
2246
|
+
const next = Object.assign({}, prev);
|
|
2247
|
+
if (value === undefined) delete next[key];
|
|
2248
|
+
else next[key] = value;
|
|
2249
|
+
return next;
|
|
2250
|
+
}),
|
|
2251
|
+
onChannelTemplate: (channelId, value) => setDelivery((prev) => {
|
|
2252
|
+
const templates = Object.assign({}, prev.channelTemplates || {});
|
|
2253
|
+
if (value && value.trim()) templates[channelId] = value;
|
|
2254
|
+
else delete templates[channelId];
|
|
2255
|
+
return Object.assign({}, prev, { channelTemplates: templates });
|
|
2256
|
+
}),
|
|
2257
|
+
sections,
|
|
2258
|
+
onToggleSection: (key) => setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] })),
|
|
2259
|
+
}),
|
|
1689
2260
|
testInfo && testInfo.message && React.createElement('div', {
|
|
1690
2261
|
className: 'dsh-cron-status-banner',
|
|
1691
2262
|
'data-type': testInfo.success ? 'success' : 'error',
|