@goodandready/dsh-cron 0.2.0 → 0.2.2
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/docs/design/DESIGN.md +4 -3
- package/lib/client.js +176 -83
- package/lib/http-utils.js +11 -5
- package/lib/index.js +58 -10
- package/lib/scheduler.js +0 -33
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
- Поисковая строка; сводная статистика (активные задачи, запуски, токены, стоимость).
|
|
17
17
|
- Карточки задач: статус-переключатель, название, расписание (человекочитаемое + raw cron), действия (запуск, редактирование, удаление).
|
|
18
18
|
- Блок «Рекомендуемые задачи»: готовые шаблоны (Daily digest, Weekly review, Follow-up monitor) в один клик.
|
|
19
|
-
- Карточка настроек в слоте settings.plugin.item, key = namespace `dsh-cron`;
|
|
19
|
+
- Карточка настроек в слоте settings.plugin.item, key = namespace `dsh-cron`; отдельный раздел настроек не используется (#102).
|
|
20
20
|
- LLM Tools: cron_create_task (+ alias cron_schedule_task), cron_list_tasks, cron_pause_task, cron_resume_task, cron_delete_task, cron_run_task.
|
|
21
21
|
- API: HTTP эндпоинты /dsh-cron/* (tasks, models, chat/start, settings, telegram/test, kanban/test, tasks/:id/actions, legacy action/:id/:action). Мутирующие эндпоинты отклоняют cross-origin запросы; script-задачи по HTTP требуют заголовок x-dsh-cron-confirm.
|
|
22
22
|
- Chat / Slash Commands: отсутствуют (ранее заявленные /cron-команды не были реализованы и удалены из документации; решение 2026-09-09).
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
- SettingsModal: настройки Telegram/Kanban с тестами доставки.
|
|
47
47
|
- TaskItem: строка задачи с переключателем состояния и действиями.
|
|
48
48
|
- RecommendationCard: плашка с готовым шаблоном.
|
|
49
|
-
- CronSettingsCard: карточка параметров плагина в
|
|
49
|
+
- CronSettingsCard: карточка параметров плагина в настройках; свёрнута по умолчанию, шапка-кнопка с шевроном разворачивает тело (aria-expanded), кнопка «Открыть панель задач» — внутри раскрытого тела (решение 2026-09-10, #100).
|
|
50
50
|
- States:
|
|
51
51
|
- Loading: индикатор загрузки списка/истории.
|
|
52
52
|
- Empty: дружелюбный пустой экран со списком рекомендаций.
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
## Locked Design Decisions
|
|
64
64
|
- 2026-09-09 — Пакет надёжности ядра (v0.1.24): буфер shell-задач 10МБ, атомарное сохранение с PID, аудит пропущенных запусков при рестарте (missed), фоновый поллинг UI (8с).
|
|
65
65
|
- 2026-09-03 — Публичный скоуп @goodandready/dsh-cron; оверлей через mountSidebarEntry/mountScreen аналогично dsh-kanban; двойная кнопка «Создать ⌄».
|
|
66
|
-
- 2026-09-09 — Слот карточки настроек: settings.plugin.item с key/namespace `dsh-cron` (совпадение с серверной регистрацией)
|
|
66
|
+
- 2026-09-09 — Слот карточки настроек: settings.plugin.item с key/namespace `dsh-cron` (совпадение с серверной регистрацией). Причина: контракт слота настроек (#85). Changed 2026-09-10 (#102): settings.section fallback удалён — карточка только во вкладке плагинов.
|
|
67
|
+
- 2026-09-10 — Значения настроек пишутся через зарегистрированный settings-скоуп (`scope.set` → `watch` → store); REST /dsh-cron/settings остаётся транспортом для карточки и headless-сценариев; карточка проверяет статус снапшота и не рисует поля до его получения (#102).
|
|
67
68
|
- 2026-09-09 — Английский — канонический язык строк; словарь STRINGS.en регистрируется в ctx.locale; русский — через translation-плагин. Причина: стандарт DSH-плагинов (#87).
|
|
68
69
|
- 2026-09-09 — Same-origin проверка мутирующих эндпоинтов, лимит тела 1 МБ, confirm-заголовок для script-задач. Причина: закрытие CSRF→RCE поверхности (#86).
|
|
69
70
|
- 2026-09-09 — Стилевая изоляция: динамические <style> с data-dsh-plugin="dsh-cron"; цвета только через токены темы + единый блок plugin-переменных. Причина: защита от очистки стилей соседями и поддержка светлой темы (#91).
|
package/lib/client.js
CHANGED
|
@@ -119,7 +119,9 @@ window.__ModuleLoader__.load({
|
|
|
119
119
|
'form.notifyOnlyFailure': 'Only on failures (onlyOnFailure / silent on success)',
|
|
120
120
|
'form.nextRunLabel': 'Next run: ',
|
|
121
121
|
'form.notScheduled': 'not scheduled',
|
|
122
|
-
'settings.
|
|
122
|
+
'settings.loading': 'Loading settings...',
|
|
123
|
+
'settings.unavailable': 'Settings are unavailable right now.',
|
|
124
|
+
'settings.retry': 'Retry',
|
|
123
125
|
'settings.cardTitle': '⏰ Task scheduler (Cron)',
|
|
124
126
|
'settings.cardDesc': 'Scheduled task execution (cron/intervals), background agent scenarios and Telegram/Kanban integration.',
|
|
125
127
|
'settings.openPanel': 'Open task panel',
|
|
@@ -170,20 +172,65 @@ window.__ModuleLoader__.load({
|
|
|
170
172
|
};
|
|
171
173
|
const T = (key, vars) => translate(key, vars);
|
|
172
174
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
+
// Core chevron primitive when the build ships it; own SVG fallback otherwise.
|
|
176
|
+
let CoreChevron = null;
|
|
177
|
+
try {
|
|
178
|
+
const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
179
|
+
CoreChevron = primitives && primitives.IconChevronDownOutline14;
|
|
180
|
+
} catch (e) {}
|
|
181
|
+
const chevronNode = () => (CoreChevron ? React.createElement(CoreChevron) : React.createElement('span', { style: { display: 'flex', alignItems: 'center' }, dangerouslySetInnerHTML: { __html: ICON_CHEVRON_DOWN } }));
|
|
182
|
+
|
|
183
|
+
// Own React error boundary (#106): the core's SlotErrorBoundary renders an
|
|
184
|
+
// empty div on crash, so a slot failure would silently vanish. Ours keeps
|
|
185
|
+
// the failure visible with a retry.
|
|
186
|
+
function createErrorBoundary() {
|
|
187
|
+
if (!React || typeof React.Component !== 'function') {
|
|
188
|
+
return function NoopBoundary(props) { return (props && props.children) || null; };
|
|
189
|
+
}
|
|
190
|
+
return class ErrorBoundary extends React.Component {
|
|
191
|
+
constructor(props) {
|
|
192
|
+
super(props);
|
|
193
|
+
this.state = { hasError: false, error: null };
|
|
194
|
+
}
|
|
195
|
+
static getDerivedStateFromError(error) {
|
|
196
|
+
return { hasError: true, error };
|
|
197
|
+
}
|
|
198
|
+
componentDidCatch(error, errorInfo) {
|
|
199
|
+
console.error('[dsh-cron] React error:', error, errorInfo);
|
|
200
|
+
}
|
|
201
|
+
render() {
|
|
202
|
+
if (this.state.hasError) {
|
|
203
|
+
return React.createElement('div', { className: 'dsh-cron-status-banner', 'data-type': 'error', style: { margin: '12px 0' } },
|
|
204
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ Cron UI error:'),
|
|
205
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String((this.state.error && this.state.error.message) || this.state.error)),
|
|
206
|
+
React.createElement('button', {
|
|
207
|
+
type: 'button',
|
|
208
|
+
className: 'dsh-cron-btn-secondary',
|
|
209
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
210
|
+
onClick: () => this.setState({ hasError: false, error: null })
|
|
211
|
+
}, 'Retry')
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return (this.props && this.props.children) || null;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const ErrorBoundary = createErrorBoundary();
|
|
219
|
+
|
|
220
|
+
// Status colors: core semantic state tokens first (same set dsh-clinebot
|
|
221
|
+
// uses), plugin fallbacks as a single point of change (#105).
|
|
175
222
|
const STYLES = `
|
|
176
223
|
:root {
|
|
177
|
-
--dsh-cron-success: var(--dsw-alias-
|
|
178
|
-
--dsh-cron-success-bg:
|
|
179
|
-
--dsh-cron-danger: var(--dsw-alias-
|
|
180
|
-
--dsh-cron-danger-bg:
|
|
181
|
-
--dsh-cron-info: var(--dsw-alias-
|
|
182
|
-
--dsh-cron-info-bg:
|
|
183
|
-
--dsh-cron-accent: var(--dsw-alias-
|
|
184
|
-
--dsh-cron-accent-bg:
|
|
185
|
-
--dsh-cron-warning: var(--dsw-alias-
|
|
186
|
-
--dsh-cron-warning-bg:
|
|
224
|
+
--dsh-cron-success: var(--dsw-alias-state-success-primary, #10b981);
|
|
225
|
+
--dsh-cron-success-bg: rgba(16, 185, 129, 0.1);
|
|
226
|
+
--dsh-cron-danger: var(--dsw-alias-state-error-primary, #ef4444);
|
|
227
|
+
--dsh-cron-danger-bg: rgba(239, 68, 68, 0.1);
|
|
228
|
+
--dsh-cron-info: var(--dsw-alias-state-brand-primary, #60a5fa);
|
|
229
|
+
--dsh-cron-info-bg: rgba(96, 165, 250, 0.12);
|
|
230
|
+
--dsh-cron-accent: var(--dsw-alias-state-info-primary, #c084fc);
|
|
231
|
+
--dsh-cron-accent-bg: rgba(168, 85, 247, 0.12);
|
|
232
|
+
--dsh-cron-warning: var(--dsw-alias-state-warning-primary, #eab308);
|
|
233
|
+
--dsh-cron-warning-bg: rgba(234, 179, 8, 0.12);
|
|
187
234
|
}
|
|
188
235
|
[data-pane=conversation], [class*=centerCol] {
|
|
189
236
|
position: relative !important;
|
|
@@ -208,43 +255,46 @@ window.__ModuleLoader__.load({
|
|
|
208
255
|
.dsh-cron-entry-label { text-overflow: ellipsis; overflow: hidden; }
|
|
209
256
|
[data-dsh-frame][data-sidebar-collapsed] .dsh-cron-entry-label { display: none; }
|
|
210
257
|
|
|
211
|
-
.dsh-cron-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--dsw-alias-bg-base, #171717); color: var(--dsw-alias-label-primary, #ededed); overflow-y: auto; padding:
|
|
212
|
-
.dsh-cron-container { max-width:
|
|
258
|
+
.dsh-cron-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--dsw-alias-bg-base, #171717); color: var(--dsw-alias-label-primary, #ededed); overflow-y: auto; padding: 24px 48px 32px; z-index: 100; box-sizing: border-box; }
|
|
259
|
+
.dsh-cron-container { max-width: 960px; width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; padding: 8px 0 32px; box-sizing: border-box; }
|
|
213
260
|
|
|
214
|
-
.dsh-cron-top-bar { display: flex; align-items: center; gap: 12px;
|
|
261
|
+
.dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; }
|
|
215
262
|
.dsh-cron-back-btn { appearance: none; font: inherit; cursor: pointer; color: var(--dsw-alias-label-secondary, #9ca3af); background: 0 0; border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 8px; align-items: center; gap: 6px; padding: 4px 12px; font-size: 13px; display: inline-flex; flex: none; height: 32px; box-sizing: border-box; }
|
|
216
|
-
.dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06)); }
|
|
263
|
+
.dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary, #ededed); background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))); }
|
|
217
264
|
|
|
218
|
-
.dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start;
|
|
219
|
-
.dsh-cron-title { font-size:
|
|
220
|
-
.dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); }
|
|
221
|
-
.dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-
|
|
265
|
+
.dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding-bottom: 16px; border-bottom: 1px solid var(--dsw-alias-border-l2, #383838); }
|
|
266
|
+
.dsh-cron-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
|
|
267
|
+
.dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary, #9ca3af); line-height: 1.5; }
|
|
268
|
+
.dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-2, #262626); color: var(--dsw-alias-label-primary, #fff); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 9999px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; position: relative; }
|
|
222
269
|
.dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover, #333)); }
|
|
223
270
|
.dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3, #212121); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 200; }
|
|
224
271
|
.dsh-cron-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; color: var(--dsw-alias-label-primary, #ededed); font-size: 13.5px; text-align: left; cursor: pointer; }
|
|
225
272
|
.dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover, #2a2a2a); }
|
|
226
|
-
.dsh-cron-search-bar { position: relative; margin-bottom:
|
|
227
|
-
.dsh-cron-search-input { width: 100%; background: var(--dsw-alias-bg-layer-
|
|
273
|
+
.dsh-cron-search-bar { position: relative; margin-bottom: 0; }
|
|
274
|
+
.dsh-cron-search-input { width: 100%; height: 36px; background: var(--dsw-alias-bg-layer-2, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 0 12px 0 38px; color: var(--dsw-alias-label-primary, inherit); font-size: 13px; outline: none; box-sizing: border-box; }
|
|
275
|
+
.dsh-cron-search-input:focus { outline: none; border-color: var(--dsh-cron-info); }
|
|
228
276
|
.dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary, #666); }
|
|
229
|
-
.dsh-cron-tabs { display: flex;
|
|
230
|
-
.dsh-cron-tab { appearance: none; background: transparent; border:
|
|
231
|
-
.dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: var(--dsw-alias-label-primary, #fff);
|
|
232
|
-
.dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom:
|
|
277
|
+
.dsh-cron-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
278
|
+
.dsh-cron-tab { appearance: none; background: transparent; border: 1px solid var(--dsw-alias-border-l2, #383838); padding: 4px 12px; border-radius: 999px; color: var(--dsw-alias-label-secondary, #9ca3af); font-size: 12px; font-weight: 500; cursor: pointer; }
|
|
279
|
+
.dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4, #2a2a2a); color: var(--dsw-alias-label-primary, #fff); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #383838)); }
|
|
280
|
+
.dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 0; }
|
|
233
281
|
.dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1e1e1e); border: 1px solid var(--dsw-alias-border-l2, #2e2e2e); border-radius: 12px; cursor: pointer; transition: background 0.15s, border-color 0.15s; }
|
|
234
|
-
.dsh-cron-task-item:hover, .dsh-cron-task-item:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-border-l1, #444); outline: none; }
|
|
235
|
-
.dsh-cron-task-prompt-preview { font-size: 12.5px; color: var(--dsw-alias-label-
|
|
236
|
-
.dsh-cron-type-tag { display: inline-
|
|
237
|
-
.dsh-cron-task-model-tag { display: inline-
|
|
282
|
+
.dsh-cron-task-item:hover, .dsh-cron-task-item:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l1, #444)); outline: none; }
|
|
283
|
+
.dsh-cron-task-prompt-preview { font-size: 12.5px; color: var(--dsw-alias-label-secondary, #888); margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; line-height: 1.3; }
|
|
284
|
+
.dsh-cron-type-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); text-transform: uppercase; font-weight: 500; margin-left: 8px; vertical-align: middle; }
|
|
285
|
+
.dsh-cron-task-model-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); color: var(--dsw-alias-label-secondary, #aaa); margin-left: 8px; vertical-align: middle; }
|
|
238
286
|
.dsh-cron-task-actions { display: flex; gap: 8px; }
|
|
239
|
-
.dsh-cron-icon-btn { background: transparent; border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; color: var(--dsw-alias-label-
|
|
240
|
-
.dsh-cron-icon-btn:hover { background: var(--dsw-alias-
|
|
241
|
-
.dsh-cron-
|
|
287
|
+
.dsh-cron-icon-btn { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; font-weight: 500; color: var(--dsw-alias-label-primary, #aaa); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
|
|
288
|
+
.dsh-cron-icon-btn:hover { background: var(--dsw-alias-bg-layer-4, #2a2a2a); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #444)); }
|
|
289
|
+
.dsh-cron-icon-btn.dsh-cron-danger { color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger-bg); }
|
|
290
|
+
.dsh-cron-icon-btn.dsh-cron-danger:hover { background: var(--dsh-cron-danger-bg); border-color: var(--dsh-cron-danger); }
|
|
291
|
+
.dsh-cron-task-status-btn { background: none; border: 1px solid var(--dsw-alias-border-l2, #444); border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; color: var(--dsw-alias-label-tertiary, #888); cursor: pointer; flex-shrink: 0; }
|
|
242
292
|
.dsh-cron-task-status-btn[data-active="true"] { color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
|
|
243
|
-
.dsh-cron-recs-title { font-size:
|
|
293
|
+
.dsh-cron-recs-title { font-size: 16px; font-weight: 600; margin-bottom: 14px; }
|
|
244
294
|
.dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
|
|
245
|
-
.dsh-cron-rec-card { display: flex; align-items: center; gap: 16px; padding:
|
|
246
|
-
.dsh-cron-rec-card:hover, .dsh-cron-rec-card:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-border-l2, #383838); outline: none; }
|
|
247
|
-
.dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-
|
|
295
|
+
.dsh-cron-rec-card { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3, #1c1c1c); border: 1px solid var(--dsw-alias-border-l2, #282828); border-radius: 12px; cursor: pointer; transition: background 0.15s; }
|
|
296
|
+
.dsh-cron-rec-card:hover, .dsh-cron-rec-card:focus-visible { background: var(--dsw-alias-bg-layer-4, #242424); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #383838)); outline: none; }
|
|
297
|
+
.dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.03)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
248
298
|
.dsh-cron-rec-content { flex: 1; min-width: 0; }
|
|
249
299
|
.dsh-cron-rec-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; }
|
|
250
300
|
.dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
|
|
@@ -255,14 +305,17 @@ window.__ModuleLoader__.load({
|
|
|
255
305
|
.dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
|
|
256
306
|
.dsh-cron-form-group { margin-bottom: 16px; }
|
|
257
307
|
.dsh-cron-form-group label { display: block; font-size: 13px; color: var(--dsw-alias-label-secondary, #aaa); margin-bottom: 6px; }
|
|
258
|
-
.dsh-cron-form-group input, .dsh-cron-form-group textarea, .dsh-cron-form-group select { width: 100%; background: var(--dsw-alias-bg-
|
|
308
|
+
.dsh-cron-form-group input, .dsh-cron-form-group textarea, .dsh-cron-form-group select { width: 100%; background: var(--dsw-alias-bg-layer-2, #141414); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 8px; padding: 8px 12px; color: var(--dsw-alias-label-primary, #fff); font-size: 13px; outline: none; box-sizing: border-box; }
|
|
309
|
+
.dsh-cron-form-group input:focus, .dsh-cron-form-group textarea:focus, .dsh-cron-form-group select:focus { outline: none; border-color: var(--dsh-cron-info); }
|
|
259
310
|
.dsh-cron-form-group select { cursor: pointer; }
|
|
260
311
|
.dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
|
|
261
312
|
.dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
262
313
|
.dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; }
|
|
263
|
-
.dsh-cron-btn-primary { background: var(--dsw-alias-label-primary, #ededed); color: var(--dsw-alias-bg-
|
|
314
|
+
.dsh-cron-btn-primary { background: var(--dsw-alias-label-primary, #ededed); color: var(--dsw-alias-bg-layer-3, #111); border: 1px solid transparent; border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
|
|
315
|
+
.dsh-cron-btn-primary:hover:not(:disabled) { opacity: 0.88; }
|
|
264
316
|
.dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
265
|
-
.dsh-cron-btn-secondary { background: transparent; color: var(--dsw-alias-label-
|
|
317
|
+
.dsh-cron-btn-secondary { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); color: var(--dsw-alias-label-primary, #999); border: 1px solid var(--dsw-alias-border-l2, #383838); border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
|
|
318
|
+
.dsh-cron-btn-secondary:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-4, #2a2a2a); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2, #444)); }
|
|
266
319
|
.dsh-cron-chat-box { position: relative; background: var(--dsw-alias-bg-base, #141414); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 12px; padding: 12px; }
|
|
267
320
|
.dsh-cron-chat-input { width: 100%; background: transparent; border: none; color: var(--dsw-alias-label-primary, #fff); font-size: 14px; min-height: 100px; resize: vertical; outline: none; box-sizing: border-box; line-height: 1.5; }
|
|
268
321
|
.dsh-cron-chat-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1, #222); }
|
|
@@ -271,10 +324,15 @@ window.__ModuleLoader__.load({
|
|
|
271
324
|
.dsh-cron-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
|
|
272
325
|
.dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
273
326
|
|
|
274
|
-
.dsh-cron-card { background: var(--dsw-alias-bg-layer-
|
|
327
|
+
.dsh-cron-card { background: var(--dsw-alias-bg-layer-3, #1f1f1f); border: 1px solid var(--dsw-alias-border-l2, #333); border-radius: 12px; padding: 18px 20px; margin-bottom: 0; display: flex; flex-direction: column; gap: 14px; }
|
|
328
|
+
.dsh-cron-card-head-btn { appearance: none; width: 100%; font: inherit; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 0; }
|
|
329
|
+
.dsh-cron-card-head-btn .dsh-cron-card-title { flex: 1; }
|
|
330
|
+
.dsh-cron-card-title { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #fff); }
|
|
331
|
+
.dsh-cron-chev { margin-left: auto; flex: none; color: var(--dsw-alias-label-tertiary, #888); transition: transform 0.16s; display: flex; align-items: center; }
|
|
332
|
+
.dsh-cron-chev-open { transform: rotate(180deg); }
|
|
333
|
+
.dsh-cron-card-body { border-top: 1px solid var(--dsw-alias-border-l2, #333); margin-top: 2px; padding-top: 14px; }
|
|
275
334
|
.dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
|
276
|
-
.dsh-cron-card-
|
|
277
|
-
.dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); margin-bottom: 16px; line-height: 1.4; }
|
|
335
|
+
.dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary, #888); line-height: 1.4; }
|
|
278
336
|
|
|
279
337
|
.dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l2, #2e2e2e); margin-bottom: 18px; padding-bottom: 2px; }
|
|
280
338
|
.dsh-cron-modal-tab { appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; padding: 8px 14px; font-size: 13.5px; color: var(--dsw-alias-label-tertiary, #888); cursor: pointer; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
|
|
@@ -283,21 +341,21 @@ window.__ModuleLoader__.load({
|
|
|
283
341
|
.dsh-cron-history-list { display: flex; flex-direction: column; gap: 10px; max-height: 420px; overflow-y: auto; padding-right: 4px; }
|
|
284
342
|
.dsh-cron-history-item { background: var(--dsw-alias-bg-base, #161616); border: 1px solid var(--dsw-alias-border-l1, #2a2a2a); border-radius: 8px; padding: 12px 14px; font-size: 12.5px; }
|
|
285
343
|
.dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
|
286
|
-
.dsh-cron-history-status { font-weight:
|
|
287
|
-
.dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); }
|
|
288
|
-
.dsh-cron-history-status[data-status="error"], .dsh-cron-history-status[data-status="timeout"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); }
|
|
289
|
-
.dsh-cron-history-status[data-status="skipped"], .dsh-cron-history-status[data-status="missed"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); }
|
|
344
|
+
.dsh-cron-history-status { font-weight: 500; font-size: 11px; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2, #383838); }
|
|
345
|
+
.dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
|
|
346
|
+
.dsh-cron-history-status[data-status="error"], .dsh-cron-history-status[data-status="timeout"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
|
|
347
|
+
.dsh-cron-history-status[data-status="skipped"], .dsh-cron-history-status[data-status="missed"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
|
|
290
348
|
.dsh-cron-history-time { color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; }
|
|
291
349
|
.dsh-cron-history-output { background: var(--dsw-alias-bg-base, #0c0c0c); border: 1px solid var(--dsw-alias-border-l1, #222); border-radius: 6px; padding: 8px 10px; font-family: monospace; font-size: 12px; color: var(--dsw-alias-label-secondary, #bbb); max-height: 140px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; margin-top: 6px; }
|
|
292
350
|
.dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary, #666); font-size: 13.5px; }
|
|
293
351
|
|
|
294
|
-
.dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(
|
|
295
|
-
.dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-
|
|
296
|
-
.dsh-cron-stat-val { font-size:
|
|
297
|
-
.dsh-cron-stat-lbl { font-size:
|
|
298
|
-
.dsh-cron-oneshot-tag { font-size: 11px; padding: 2px
|
|
299
|
-
.dsh-cron-cost-tag { font-size: 11px; padding: 2px
|
|
300
|
-
.dsh-cron-tokens-tag { font-size: 11px; padding: 2px
|
|
352
|
+
.dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 0; }
|
|
353
|
+
.dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.02)); border: 1px solid var(--dsw-alias-border-l2, #222); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
|
|
354
|
+
.dsh-cron-stat-val { font-size: 18px; font-weight: 700; color: var(--dsw-alias-label-primary, #fff); }
|
|
355
|
+
.dsh-cron-stat-lbl { font-size: 12px; color: var(--dsw-alias-label-secondary, #777); margin-top: 0; }
|
|
356
|
+
.dsh-cron-oneshot-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-accent); background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); font-weight: 500; }
|
|
357
|
+
.dsh-cron-cost-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-success); background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); font-weight: 500; }
|
|
358
|
+
.dsh-cron-tokens-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-info); background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); }
|
|
301
359
|
.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; }
|
|
302
360
|
.dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary, #ededed); }
|
|
303
361
|
.dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); margin-top: 4px; }
|
|
@@ -930,7 +988,7 @@ window.__ModuleLoader__.load({
|
|
|
930
988
|
}, React.createElement('span', svgProps(ICON_EDIT))),
|
|
931
989
|
React.createElement('button', {
|
|
932
990
|
type: 'button',
|
|
933
|
-
className: 'dsh-cron-icon-btn',
|
|
991
|
+
className: 'dsh-cron-icon-btn dsh-cron-danger',
|
|
934
992
|
title: T_KEY(t, 'actions.delete'),
|
|
935
993
|
'aria-label': T_KEY(t, 'actions.delete'),
|
|
936
994
|
onClick: () => handleDelete(task.id)
|
|
@@ -1355,24 +1413,36 @@ window.__ModuleLoader__.load({
|
|
|
1355
1413
|
const [hasDefault, setHasDefault] = React.useState(false);
|
|
1356
1414
|
const [saved, setSaved] = React.useState(false);
|
|
1357
1415
|
const [testInfo, setTestInfo] = React.useState(null);
|
|
1416
|
+
// Collapsed by default, head is the toggle (canonical card contract, #100)
|
|
1417
|
+
const [cardOpen, setCardOpen] = React.useState(false);
|
|
1418
|
+
|
|
1419
|
+
// Settings snapshot status (#102): never render phantom inputs before
|
|
1420
|
+
// the snapshot arrives; offer a retry when it is unavailable.
|
|
1421
|
+
const [loadState, setLoadState] = React.useState('loading');
|
|
1422
|
+
const [reloadTick, setReloadTick] = React.useState(0);
|
|
1358
1423
|
|
|
1359
1424
|
React.useEffect(() => {
|
|
1360
1425
|
let active = true;
|
|
1426
|
+
setLoadState('loading');
|
|
1361
1427
|
fetch('/dsh-cron/settings')
|
|
1362
1428
|
.then(r => r.json())
|
|
1363
1429
|
.then(data => {
|
|
1364
|
-
if (active
|
|
1430
|
+
if (!active) return;
|
|
1431
|
+
if (data && data.ok && data.settings) {
|
|
1365
1432
|
setToken(data.settings.botToken || '');
|
|
1366
1433
|
setChatId(data.settings.chatId || '');
|
|
1367
1434
|
setNotify(Boolean(data.settings.notifyTelegram));
|
|
1368
1435
|
setOnlyFailure(Boolean(data.settings.onlyOnFailure));
|
|
1369
1436
|
setKanbanUrl(data.settings.kanbanBaseUrl || 'http://127.0.0.1:3000');
|
|
1370
1437
|
setHasDefault(Boolean(data.settings.hasDefaultCredentials));
|
|
1438
|
+
setLoadState('ready');
|
|
1439
|
+
} else {
|
|
1440
|
+
setLoadState('error');
|
|
1371
1441
|
}
|
|
1372
1442
|
})
|
|
1373
|
-
.catch(() => {});
|
|
1443
|
+
.catch(() => { if (active) setLoadState('error'); });
|
|
1374
1444
|
return () => { active = false; };
|
|
1375
|
-
}, []);
|
|
1445
|
+
}, [reloadTick]);
|
|
1376
1446
|
|
|
1377
1447
|
const saveSettings = async (e) => {
|
|
1378
1448
|
if (e) e.preventDefault();
|
|
@@ -1434,19 +1504,44 @@ window.__ModuleLoader__.load({
|
|
|
1434
1504
|
}
|
|
1435
1505
|
};
|
|
1436
1506
|
|
|
1507
|
+
if (loadState !== 'ready') {
|
|
1508
|
+
return React.createElement('div', { className: 'dsh-cron-card' },
|
|
1509
|
+
React.createElement('div', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
|
|
1510
|
+
React.createElement('div', {
|
|
1511
|
+
className: 'dsh-cron-status-banner',
|
|
1512
|
+
'data-type': loadState === 'error' ? 'error' : 'info',
|
|
1513
|
+
style: { marginTop: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '10px' }
|
|
1514
|
+
},
|
|
1515
|
+
React.createElement('span', null, loadState === 'error' ? T_KEY(t, 'settings.unavailable') : T_KEY(t, 'settings.loading')),
|
|
1516
|
+
loadState === 'error' && React.createElement('button', {
|
|
1517
|
+
type: 'button',
|
|
1518
|
+
className: 'dsh-cron-btn-secondary',
|
|
1519
|
+
onClick: () => setReloadTick(x => x + 1)
|
|
1520
|
+
}, T_KEY(t, 'settings.retry'))
|
|
1521
|
+
)
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1437
1525
|
return React.createElement('div', { className: 'dsh-cron-card' },
|
|
1438
|
-
React.createElement('
|
|
1526
|
+
React.createElement('button', {
|
|
1527
|
+
type: 'button',
|
|
1528
|
+
className: 'dsh-cron-card-head-btn',
|
|
1529
|
+
'aria-expanded': cardOpen ? 'true' : 'false',
|
|
1530
|
+
onClick: () => setCardOpen(!cardOpen)
|
|
1531
|
+
},
|
|
1439
1532
|
React.createElement('span', { className: 'dsh-cron-card-title' }, T_KEY(t, 'settings.cardTitle')),
|
|
1440
|
-
React.createElement('
|
|
1441
|
-
type: 'button',
|
|
1442
|
-
className: 'dsh-cron-btn-primary',
|
|
1443
|
-
onClick: () => { if (toggle) toggle.set(true); }
|
|
1444
|
-
}, T_KEY(t, 'settings.openPanel'))
|
|
1445
|
-
),
|
|
1446
|
-
React.createElement('div', { className: 'dsh-cron-card-desc', style: { marginBottom: '16px' } },
|
|
1447
|
-
T_KEY(t, 'settings.cardDesc')
|
|
1533
|
+
React.createElement('span', { className: 'dsh-cron-chev' + (cardOpen ? ' dsh-cron-chev-open' : '') }, chevronNode())
|
|
1448
1534
|
),
|
|
1449
|
-
React.createElement('
|
|
1535
|
+
cardOpen && React.createElement('div', { className: 'dsh-cron-card-body' },
|
|
1536
|
+
React.createElement('div', { className: 'dsh-cron-card-desc' }, T_KEY(t, 'settings.cardDesc')),
|
|
1537
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: '12px' } },
|
|
1538
|
+
React.createElement('button', {
|
|
1539
|
+
type: 'button',
|
|
1540
|
+
className: 'dsh-cron-btn-primary',
|
|
1541
|
+
onClick: () => { if (toggle) toggle.set(true); }
|
|
1542
|
+
}, T_KEY(t, 'settings.openPanel'))
|
|
1543
|
+
),
|
|
1544
|
+
React.createElement('form', { onSubmit: saveSettings, style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
|
|
1450
1545
|
React.createElement('div', { className: 'dsh-cron-form-group', style: { marginBottom: 0 } },
|
|
1451
1546
|
React.createElement('label', null, T_KEY(t, 'settings.tokenLabel')),
|
|
1452
1547
|
React.createElement('input', {
|
|
@@ -1516,7 +1611,8 @@ window.__ModuleLoader__.load({
|
|
|
1516
1611
|
}, T_KEY(t, 'settings.testKanban'))
|
|
1517
1612
|
)
|
|
1518
1613
|
)
|
|
1519
|
-
)
|
|
1614
|
+
)
|
|
1615
|
+
);
|
|
1520
1616
|
}
|
|
1521
1617
|
|
|
1522
1618
|
function nativeButton() {
|
|
@@ -1613,7 +1709,7 @@ window.__ModuleLoader__.load({
|
|
|
1613
1709
|
column.appendChild(container);
|
|
1614
1710
|
if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
|
|
1615
1711
|
root = ReactDOM.createRoot(container);
|
|
1616
|
-
root.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }));
|
|
1712
|
+
root.render(React.createElement(ErrorBoundary, null, React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate })));
|
|
1617
1713
|
} else if (ReactDOM && typeof ReactDOM.render === 'function') {
|
|
1618
1714
|
ReactDOM.render(React.createElement(CronScreen, { ctx, toggle, onClose: () => toggle.set(false), t: translate }), container);
|
|
1619
1715
|
}
|
|
@@ -1696,23 +1792,20 @@ window.__ModuleLoader__.load({
|
|
|
1696
1792
|
|
|
1697
1793
|
const toggle = createToggle();
|
|
1698
1794
|
|
|
1795
|
+
// The settings card lives ONLY in the plugins settings tab (#102): no
|
|
1796
|
+
// top-level section fallback — the flat sidebar list is a shared core
|
|
1797
|
+
// resource, and the card slot is proven on current builds (#85).
|
|
1798
|
+
function SettingsCardWithBoundary(props) {
|
|
1799
|
+
return React.createElement(ErrorBoundary, null, CronSettingsCard(props));
|
|
1800
|
+
}
|
|
1699
1801
|
const cardOk = registerIntoMount(ctx, 'settings.plugin.item', {
|
|
1700
1802
|
name: 'settings.plugin.item',
|
|
1701
1803
|
key: NS,
|
|
1702
1804
|
locale: NS,
|
|
1703
1805
|
inject: () => ({ ctx, toggle }),
|
|
1704
|
-
},
|
|
1806
|
+
}, SettingsCardWithBoundary);
|
|
1705
1807
|
if (!cardOk) {
|
|
1706
|
-
|
|
1707
|
-
// dedicated section so the settings are never lost (#85).
|
|
1708
|
-
registerIntoMount(ctx, 'settings.section', {
|
|
1709
|
-
name: 'settings.section',
|
|
1710
|
-
id: PLUGIN_ID,
|
|
1711
|
-
order: 34,
|
|
1712
|
-
locale: NS,
|
|
1713
|
-
label: () => translate('settings.sectionLabel'),
|
|
1714
|
-
inject: () => ({ ctx, toggle }),
|
|
1715
|
-
}, CronSettingsCard);
|
|
1808
|
+
console.warn('[dsh-cron] settings.plugin.item slot unavailable — settings card not registered');
|
|
1716
1809
|
}
|
|
1717
1810
|
|
|
1718
1811
|
const chipOk = registerIntoMount(ctx, 'conversation.session.header.utilities', {
|
|
@@ -1722,7 +1815,7 @@ window.__ModuleLoader__.load({
|
|
|
1722
1815
|
locale: NS,
|
|
1723
1816
|
inject: () => ({ ctx, toggle }),
|
|
1724
1817
|
}, CronChip);
|
|
1725
|
-
const cardSlot = cardOk ? 'settings.plugin.item' :
|
|
1818
|
+
const cardSlot = cardOk ? 'settings.plugin.item' : undefined;
|
|
1726
1819
|
const chipSlot = chipOk ? 'conversation.session.header.utilities' : undefined;
|
|
1727
1820
|
|
|
1728
1821
|
function CronChip(props) {
|
package/lib/http-utils.js
CHANGED
|
@@ -8,12 +8,15 @@ export const MAX_BODY_BYTES = 1024 * 1024;
|
|
|
8
8
|
export const SCRIPT_CONFIRM_HEADER = 'x-dsh-cron-confirm';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Parse a JSON request body with a hard size cap.
|
|
11
|
+
* Parse a JSON request body with a hard byte size cap.
|
|
12
|
+
* Chunks are accumulated as Buffers so multi-byte UTF-8 characters split
|
|
13
|
+
* across chunk boundaries decode correctly (#106, #97 follow-up).
|
|
12
14
|
* Rejects with err.statusCode = 413 when the body exceeds the limit.
|
|
13
15
|
*/
|
|
14
16
|
export function parseJsonBody(req, limit = MAX_BODY_BYTES) {
|
|
15
17
|
return new Promise((resolve, reject) => {
|
|
16
|
-
|
|
18
|
+
const chunks = [];
|
|
19
|
+
let bytes = 0;
|
|
17
20
|
let settled = false;
|
|
18
21
|
const fail = (err) => {
|
|
19
22
|
if (settled) return;
|
|
@@ -22,17 +25,20 @@ export function parseJsonBody(req, limit = MAX_BODY_BYTES) {
|
|
|
22
25
|
};
|
|
23
26
|
req.on('data', (chunk) => {
|
|
24
27
|
if (settled) return;
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
29
|
+
bytes += buf.length;
|
|
30
|
+
if (bytes > limit) {
|
|
27
31
|
fail(Object.assign(new Error('Request body exceeds the allowed size'), { statusCode: 413 }));
|
|
28
32
|
req.destroy();
|
|
33
|
+
return;
|
|
29
34
|
}
|
|
35
|
+
chunks.push(buf);
|
|
30
36
|
});
|
|
31
37
|
req.on('end', () => {
|
|
32
38
|
if (settled) return;
|
|
33
39
|
settled = true;
|
|
34
40
|
try {
|
|
35
|
-
resolve(
|
|
41
|
+
resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {});
|
|
36
42
|
} catch (err) {
|
|
37
43
|
reject(err);
|
|
38
44
|
}
|
package/lib/index.js
CHANGED
|
@@ -66,6 +66,37 @@ async function readBody(req, res) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Apply settings writes through the registered settings scope so the core
|
|
71
|
+
* snapshot stays authoritative (#102). Returns null when the settings
|
|
72
|
+
* service is unavailable (headless profiles fall back to store-only writes).
|
|
73
|
+
*/
|
|
74
|
+
export async function applySettingsToScope(settingsScope, body) {
|
|
75
|
+
if (!settingsScope || typeof settingsScope.set !== 'function') return null;
|
|
76
|
+
const errors = [];
|
|
77
|
+
for (const key of ['botToken', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl']) {
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
|
|
79
|
+
try {
|
|
80
|
+
await settingsScope.set(key, body[key]);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
errors.push(key + ': ' + ((e && e.message) || e));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return errors;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Drop values that must never re-enter the store from the client: a masked
|
|
90
|
+
* secret echoed back by the settings UI would overwrite the real token.
|
|
91
|
+
*/
|
|
92
|
+
export function sanitizeSettingsPayload(body) {
|
|
93
|
+
const payload = Object.assign({}, body);
|
|
94
|
+
if (typeof payload.botToken === 'string' && (payload.botToken.includes('••') || payload.botToken.includes('****'))) {
|
|
95
|
+
delete payload.botToken;
|
|
96
|
+
}
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
/**
|
|
70
101
|
* Shared implementation for the /dsh-cron/tasks... and /dsh-cron/action...
|
|
71
102
|
* route families — the #66 alias is kept alive by the same code path instead
|
|
@@ -73,9 +104,9 @@ async function readBody(req, res) {
|
|
|
73
104
|
*
|
|
74
105
|
* IMPORTANT: prefix routes must be registered WITHOUT a trailing slash —
|
|
75
106
|
* with one, the core degrades matching to exact-path equality and every
|
|
76
|
-
* deeper path unrouted (#96).
|
|
107
|
+
* deeper path unrouted (#96). Exported for unit tests.
|
|
77
108
|
*/
|
|
78
|
-
function createCronApiHandler(store, scheduler, collection) {
|
|
109
|
+
export function createCronApiHandler(store, scheduler, collection) {
|
|
79
110
|
return async function handleCronApi(req, res) {
|
|
80
111
|
try {
|
|
81
112
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
@@ -155,6 +186,10 @@ function createCronApiHandler(store, scheduler, collection) {
|
|
|
155
186
|
}
|
|
156
187
|
|
|
157
188
|
if (req.method === 'POST' && action === 'run') {
|
|
189
|
+
if (!store.get(id)) {
|
|
190
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
158
193
|
await scheduler.triggerManualRun(id);
|
|
159
194
|
sendJson(res, 200, { ok: true, task: store.get(id) });
|
|
160
195
|
return;
|
|
@@ -240,9 +275,9 @@ function createCronApiHandler(store, scheduler, collection) {
|
|
|
240
275
|
|
|
241
276
|
/**
|
|
242
277
|
* Single implementation behind the cron_create_task / cron_schedule_task
|
|
243
|
-
* alias pair (#70, #92).
|
|
278
|
+
* alias pair (#70, #92). Exported for unit tests.
|
|
244
279
|
*/
|
|
245
|
-
function executeCreateTask(store, scheduler, args) {
|
|
280
|
+
export function executeCreateTask(store, scheduler, args) {
|
|
246
281
|
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
247
282
|
const parsed = parseScheduleExpression(args.schedule);
|
|
248
283
|
const task = store.set({
|
|
@@ -303,19 +338,26 @@ export function apply(ctx, config) {
|
|
|
303
338
|
const runner = new SessionRunner(ctx);
|
|
304
339
|
const scheduler = new TaskScheduler(store, (task, opts) => runner.execute(task, opts));
|
|
305
340
|
|
|
306
|
-
// Register settings in Harness settings service
|
|
341
|
+
// Register settings in the Harness settings service (#71, #102): the scope
|
|
342
|
+
// is the source of truth for the core, scope.watch mirrors values into the
|
|
343
|
+
// plugin store, and REST writes go through scope.set so nothing bypasses
|
|
344
|
+
// the settings service.
|
|
345
|
+
let cronSettingsScope = null;
|
|
307
346
|
ctx.inject(['settings'], (sctx) => {
|
|
308
347
|
try {
|
|
309
|
-
|
|
348
|
+
cronSettingsScope = sctx.settings.register('dsh-cron', Config, {
|
|
310
349
|
base: store.getSettings()
|
|
311
350
|
});
|
|
312
|
-
if (
|
|
313
|
-
|
|
351
|
+
if (cronSettingsScope && typeof cronSettingsScope.watch === 'function') {
|
|
352
|
+
cronSettingsScope.watch((updated) => {
|
|
314
353
|
if (updated && typeof updated === 'object') {
|
|
315
354
|
store.saveSettings(updated);
|
|
316
355
|
}
|
|
317
356
|
});
|
|
318
357
|
}
|
|
358
|
+
sctx.effect(() => () => {
|
|
359
|
+
cronSettingsScope = null;
|
|
360
|
+
});
|
|
319
361
|
} catch (e) {
|
|
320
362
|
console.warn('[dsh-cron] settings registration warning:', e.message);
|
|
321
363
|
}
|
|
@@ -407,8 +449,14 @@ export function apply(ctx, config) {
|
|
|
407
449
|
if (rejectCrossOrigin(req, res)) return;
|
|
408
450
|
const { body, error } = await readBody(req, res);
|
|
409
451
|
if (error) return;
|
|
410
|
-
|
|
411
|
-
|
|
452
|
+
// Masked secrets echoed back by the client must never overwrite the
|
|
453
|
+
// stored token (store.saveSettings guards this too, #86-era).
|
|
454
|
+
const payload = sanitizeSettingsPayload(body);
|
|
455
|
+
const scopeErrors = await applySettingsToScope(cronSettingsScope, payload);
|
|
456
|
+
if (scopeErrors === null) {
|
|
457
|
+
store.saveSettings(payload);
|
|
458
|
+
}
|
|
459
|
+
sendJson(res, 200, { ok: true, settings: store.getClientSettings(), scopeErrors: scopeErrors || undefined });
|
|
412
460
|
return;
|
|
413
461
|
}
|
|
414
462
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
package/lib/scheduler.js
CHANGED
|
@@ -434,40 +434,7 @@ export class TaskScheduler {
|
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
removeTask(taskId) {
|
|
438
|
-
if (this.jobs.has(taskId)) {
|
|
439
|
-
this.jobs.get(taskId).stop();
|
|
440
|
-
this.jobs.delete(taskId);
|
|
441
|
-
}
|
|
442
|
-
if (this.timers.has(taskId)) {
|
|
443
|
-
clearTimeout(this.timers.get(taskId));
|
|
444
|
-
this.timers.delete(taskId);
|
|
445
|
-
}
|
|
446
|
-
const running = this.running.get(taskId);
|
|
447
|
-
if (running) {
|
|
448
|
-
try {
|
|
449
|
-
running.controller.abort(new Error('Task deleted'));
|
|
450
|
-
} catch {}
|
|
451
|
-
this.running.delete(taskId);
|
|
452
|
-
}
|
|
453
|
-
return this.store.delete(taskId);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
437
|
triggerManualRun(taskId) {
|
|
457
438
|
return this.runTask(taskId);
|
|
458
439
|
}
|
|
459
|
-
|
|
460
|
-
getNextRun(schedule) {
|
|
461
|
-
try {
|
|
462
|
-
const parsed = parseScheduleExpression(schedule);
|
|
463
|
-
if (parsed.isOneShot) {
|
|
464
|
-
return parsed.targetTimestamp;
|
|
465
|
-
}
|
|
466
|
-
const tempJob = new Cron(parsed.cronPattern);
|
|
467
|
-
const next = tempJob.nextRun();
|
|
468
|
-
return next ? next.getTime() : null;
|
|
469
|
-
} catch {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
440
|
}
|