@goodandready/dsh-cron 0.2.14 → 0.2.16
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 +17 -0
- package/README.ru.md +17 -0
- package/README.zh.md +17 -0
- package/lib/api.js +3 -1
- package/lib/channels.js +3 -13
- package/lib/chat-start.js +6 -2
- package/lib/client.js +121 -5
- package/lib/failure-inspector.js +2 -8
- package/lib/index.js +18 -0
- package/lib/recipes.js +1 -1
- package/lib/runtimes.js +2 -2
- package/lib/scheduler.js +2 -2
- package/lib/silent-rule.js +4 -1
- package/lib/telegram.js +1 -1
- package/lib/updater.js +237 -0
- package/package.json +5 -4
- package/docs/README.ru.md +0 -512
- package/docs/README.zh.md +0 -512
package/README.md
CHANGED
|
@@ -382,6 +382,22 @@ Developer-facing, no behaviour change. `parseScheduleExpression` was split into
|
|
|
382
382
|
|
|
383
383
|
---
|
|
384
384
|
|
|
385
|
+
### 27. Modal Dialog Viewport Bounds & Package Hygiene (v0.2.15, #153, #148, #151, #152)
|
|
386
|
+
- **Viewport Height Bounding & Sticky Footer**: Modal forms (including task creation, editing, and ready recipe presets) now strictly respect viewport boundaries with `max-height: min(90vh, calc(100vh - 36px))` and smooth internal scrolling. The modal action footer (`Cancel`, `Save`, `Create`) is pinned via sticky positioning (`position: sticky`), guaranteeing that critical buttons remain immediately accessible and never clipped regardless of form complexity or display scale.
|
|
387
|
+
- **Overlay Scroll Protection**: Modal overlays now provide safe viewport padding and overflow handling (`overflow-y: auto`), preventing flexbox centering clipping on small screens.
|
|
388
|
+
- **Package Weight Optimization**: Removed redundant documentation duplicates from npm distribution files, trimming tarball weight by over 32 kB and unpacked size by ~102 kB.
|
|
389
|
+
- **Cordis Client Inject Compliance**: Explicitly declared `locale` and `slots` dependencies in `package.json` client injection manifest (`dsh.client.inject`).
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
### 28. In-App One-Click Auto-Updater & Error Resilience (v0.2.16, #147, #155, #156)
|
|
396
|
+
- **Plugin Self-Updater (#147)**: Added in-app one-click self-updating module (`lib/updater.js`) with `/api/dsh-cron/update` endpoint and dedicated Settings UI card. Automatically queries npm registry, compares semver versions including pre-releases, and upgrades `@goodandready/dsh-cron` in-place through the DSH CLI without manual SSH sessions. POST updates are protected via origin validation (`rejectCrossOrigin`).
|
|
397
|
+
- **Silent Failure Elimination (#156)**: Replaced silent empty catches with diagnostic logging: uncompleted import transaction rollbacks are surfaced to system logs with warning severity, dynamic core module fallback reasons are logged, and session opening failures inform the user with actionable notifications.
|
|
398
|
+
- **Dead Code & Export Hygiene (#155)**: Cleaned unused legacy helpers (`CHANNEL_LABELS`, `makeInspectAsk`), stripped unnecessary exports from 16 internal modules, and wired `supportsSilentRule` directly into execution pipeline.
|
|
399
|
+
|
|
400
|
+
---
|
|
385
401
|
|
|
386
402
|
## 📦 Installation
|
|
387
403
|
|
|
@@ -494,6 +510,7 @@ All endpoints are served by the DSH web server under `/dsh-cron/`. Read endpoint
|
|
|
494
510
|
| `POST` | `/dsh-cron/kanban/test` | Create a Kanban connectivity-test card |
|
|
495
511
|
| `*` | `/dsh-cron/action/:id/:action` | Legacy alias for the task action routes (`run`, `toggle`, `delete`, `history`) |
|
|
496
512
|
| `GET` | `/dsh-cron/metrics` | Prometheus text exposition of task and run counters — never prompts or output (#53) |
|
|
513
|
+
| `GET` / `POST` | `/api/dsh-cron/update` | One-click plugin self-updater: query registry version and in-place upgrade (#147) |
|
|
497
514
|
| `GET` / `POST` | `/dsh-cron/api/tasks` | External token-guarded surface: list / create-or-update (#54) |
|
|
498
515
|
| `GET` / `DELETE` | `/dsh-cron/api/tasks/:id` | External token-guarded surface: read / delete (#54) |
|
|
499
516
|
| `POST` | `/dsh-cron/api/tasks/:id/run` | External token-guarded surface: force a run (#54) |
|
package/README.ru.md
CHANGED
|
@@ -258,6 +258,22 @@ cron_create_task({
|
|
|
258
258
|
|
|
259
259
|
---
|
|
260
260
|
|
|
261
|
+
### 27. Ограничение высоты модальных окон и гигиена пакета (v0.2.15, #153, #148, #151, #152)
|
|
262
|
+
- **Ограничение по высоте экрана и липкий подвал**: Модальные окна (включая создание, редактирование и готовые шаблоны-рецепты) теперь строго ограничены высотой экрана `max-height: min(90vh, calc(100vh - 36px))` с плавным внутренним скроллом. Подвал окна с кнопками («Отмена», «Сохранить», «Создать») закреплён через `position: sticky`, гарантируя постоянную доступность кнопок действий при любой длине формы и любом разрешении экрана.
|
|
263
|
+
- **Защита оверлея от вылетов**: Оверлей модального окна получил безопасные отступы и свойство `overflow-y: auto`, исключая срезание контента при flexbox-центрировании на компактных дисплеях.
|
|
264
|
+
- **Оптимизация размера npm-пакета**: Удалены дублирующие файлы документации из списка дистрибуции npm, снизив вес архива более чем на 32 kB, а распакованный размер — на 102 kB.
|
|
265
|
+
- **Соответствие манифеста клиента Cordis**: В `package.json` явно задекларированы зависимости инжекции `locale` и `slots` в секции `dsh.client.inject`.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
### 28. Модуль самообновления в 1 клик и устойчивость к ошибкам (v0.2.16, #147, #155, #156)
|
|
272
|
+
- **Самообновление плагина (#147)**: Добавлен встроенный модуль обновления (`lib/updater.js`) с маршрутом `/api/dsh-cron/update` и карточкой в настройках Web UI. Автоматически опрашивает npm registry, корректно сравнивает semver-версии (включая пререлизы) и обновляет `@goodandready/dsh-cron` на лету через CLI DSH без необходимости входа по SSH. Запросы POST защищены проверкой источника (`rejectCrossOrigin`).
|
|
273
|
+
- **Устранение скрытых сбоев (#156)**: Ликвидированы пустые `catch`: незавершённые откаты транзакций импорта логируются с уровнем `warn`, причины fallback для динамических модулей ядра фиксируются в отладочном журнале, а при сбое открытия сессии пользователь получает понятное уведомление.
|
|
274
|
+
- **Гигиена кода и экспортов (#155)**: Удалены устаревшие неиспользуемые сущности (`CHANNEL_LABELS`, `makeInspectAsk`), снят лишний модификатор `export` с 16 внутренних функций и констант, а метод `supportsSilentRule` напрямую подключен в боевой пайплайн проверки тихих правил.
|
|
275
|
+
|
|
276
|
+
---
|
|
261
277
|
|
|
262
278
|
## 📦 Установка
|
|
263
279
|
|
|
@@ -491,6 +507,7 @@ bash deploy.sh verify [exact-version]
|
|
|
491
507
|
| `POST` | `/dsh-cron/kanban/test` | Тестовая карточка в Kanban |
|
|
492
508
|
| `*` | `/dsh-cron/action/:id/:action` | Legacy-алиас действий над задачей (`run`, `toggle`, `delete`, `history`) |
|
|
493
509
|
| `GET` | `/dsh-cron/metrics` | Текст в формате Prometheus: счётчики задач и запусков — без промптов и вывода (#53) |
|
|
510
|
+
| `GET` / `POST` | `/api/dsh-cron/update` | Самообновление плагина в 1 клик: запрос версии в реестре и обновление на лету (#147) |
|
|
494
511
|
| `GET` / `POST` | `/dsh-cron/api/tasks` | Внешняя поверхность под токеном: список / создание-обновление (#54) |
|
|
495
512
|
| `GET` / `DELETE` | `/dsh-cron/api/tasks/:id` | Внешняя поверхность под токеном: чтение / удаление (#54) |
|
|
496
513
|
| `POST` | `/dsh-cron/api/tasks/:id/run` | Внешняя поверхность под токеном: принудительный запуск (#54) |
|
package/README.zh.md
CHANGED
|
@@ -381,6 +381,22 @@ bash deploy.sh verify [exact-version]
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
+
### 27. 弹窗视口高度自适应与包体积精简 (v0.2.15, #153, #148, #151, #152)
|
|
385
|
+
- **视口高度约束与粘性底部操作栏**:所有弹窗(包括任务编辑、新建及推荐模板预设)现已严格受控于视口尺寸 `max-height: min(90vh, calc(100vh - 36px))`,并内置平滑纵向滚动条。弹窗操作栏(“取消”、“保存”、“创建”)采用 `position: sticky` 底部悬浮固定,确保无论表单项多长或屏幕分辨率高低,操作按钮始终清晰可见且可随时点击。
|
|
386
|
+
- **遮罩层滚动溢出保护**:弹窗遮罩层增加了安全边距与 `overflow-y: auto`,防止小屏设备在 Flex 居中时发生头部或底部截断。
|
|
387
|
+
- **npm 包体积深度精简**:从 npm 分发清单中剔除了多余的重复文档副本,使 tarball 体积立减 32 kB 以上,解压后体积减少约 102 kB。
|
|
388
|
+
- **Cordis 客户端注入依赖规范化**:在 `package.json` 的 `dsh.client.inject` 中完整声明了 `locale` 和 `slots` 服务依赖。
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
### 28. 应用内一键自更新与错误容错增强 (v0.2.16, #147, #155, #156)
|
|
395
|
+
- **插件自更新模块 (#147)**:新增应用内一键自更新机制 (`lib/updater.js`)、`/api/dsh-cron/update` 接口与设置面板专属卡片。自动轮询 npmjs 仓库最新版本,精准比对 Semver 版本号(含预发布版本),通过 DSH CLI 就地升级 `@goodandready/dsh-cron`,无需 SSH 终端操作。POST 请求受跨域与来源保护 (`rejectCrossOrigin`)。
|
|
396
|
+
- **消除静默异常与错误追踪 (#156)**:全面消除空 `catch` 异常压制:任务导入事务回滚失败记录 `warn` 级别日志、动态核心模块降级原因记录诊断日志、会话自动唤出失败向用户呈现友好提示。
|
|
397
|
+
- **死代码清理与模块导出精简 (#155)**:清理未引用的旧版实体 (`CHANNEL_LABELS`, `makeInspectAsk`),收回 16 个内部工具函数的暴露权限,并将 `supportsSilentRule` 接入静默规则核心执行流水线。
|
|
398
|
+
|
|
399
|
+
---
|
|
384
400
|
|
|
385
401
|
## 📦 安装
|
|
386
402
|
|
|
@@ -491,6 +507,7 @@ dsh-cron:
|
|
|
491
507
|
| `POST` | `/dsh-cron/kanban/test` | 创建 Kanban 连通性测试卡片 |
|
|
492
508
|
| `*` | `/dsh-cron/action/:id/:action` | 任务操作路由的兼容别名(`run`、`toggle`、`delete`、`history`) |
|
|
493
509
|
| `GET` | `/dsh-cron/metrics` | Prometheus 文本格式的任务与运行计数 —— 不含提示词与输出(#53) |
|
|
510
|
+
| `GET` / `POST` | `/api/dsh-cron/update` | 插件一键自更新:查询仓库最新版本并就地平滑升级 (#147) |
|
|
494
511
|
| `GET` / `POST` | `/dsh-cron/api/tasks` | 令牌保护的外部接口:列表 / 创建或更新(#54) |
|
|
495
512
|
| `GET` / `DELETE` | `/dsh-cron/api/tasks/:id` | 令牌保护的外部接口:读取 / 删除(#54) |
|
|
496
513
|
| `POST` | `/dsh-cron/api/tasks/:id/run` | 令牌保护的外部接口:强制执行(#54) |
|
package/lib/api.js
CHANGED
|
@@ -343,7 +343,9 @@ function applyImportPlan({ store, scheduler, plan }) {
|
|
|
343
343
|
try {
|
|
344
344
|
if (entry.previous) store.set(entry.previous);
|
|
345
345
|
else store.delete(entry.id);
|
|
346
|
-
} catch (
|
|
346
|
+
} catch (rollbackErr) {
|
|
347
|
+
console.warn(`[dsh-cron] import rollback failed for task "${entry.id}":`, rollbackErr?.message || rollbackErr);
|
|
348
|
+
}
|
|
347
349
|
}
|
|
348
350
|
return { imported: [], error: `Import failed and was rolled back: ${writeErr.message}` };
|
|
349
351
|
}
|
package/lib/channels.js
CHANGED
|
@@ -13,22 +13,12 @@ import { resolveTemplateText, truncateText } from './templates.js';
|
|
|
13
13
|
|
|
14
14
|
export const CHANNEL_IDS = ['telegram', 'kanban', 'discord', 'slack', 'ntfy', 'bark', 'pushplus', 'tts', 'gitea'];
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
telegram: 'Telegram',
|
|
18
|
-
kanban: 'dsh-kanban card',
|
|
19
|
-
discord: 'Discord webhook',
|
|
20
|
-
slack: 'Slack webhook',
|
|
21
|
-
ntfy: 'ntfy push',
|
|
22
|
-
bark: 'Bark push',
|
|
23
|
-
pushplus: 'PushPlus',
|
|
24
|
-
tts: 'Voice via dsh-tts',
|
|
25
|
-
gitea: 'Gitea issue',
|
|
26
|
-
};
|
|
16
|
+
// CHANNEL_LABELS removed (#155) — localized via lib/client.js
|
|
27
17
|
|
|
28
18
|
const isFailed = (status) => status === 'error' || status === 'timeout';
|
|
29
19
|
|
|
30
20
|
/** The message text for a channel: custom template, else built-in defaults. */
|
|
31
|
-
|
|
21
|
+
function messageTextFor(channelId, task, runInfo, settings = {}) {
|
|
32
22
|
const templates = (settings && settings.channelTemplates) || {};
|
|
33
23
|
const template = templates[channelId] || settings.template || '';
|
|
34
24
|
return resolveTemplateText({ template, task, runInfo });
|
|
@@ -247,7 +237,7 @@ async function postJson(http, url, headers, body, channelId, signal, timeoutMs)
|
|
|
247
237
|
* One handler per channel. Each returns a small detail object and throws a
|
|
248
238
|
* descriptive Error on failure; the router turns that into a failure entry.
|
|
249
239
|
*/
|
|
250
|
-
|
|
240
|
+
const CHANNEL_HANDLERS = {
|
|
251
241
|
async telegram({ task, runInfo, settings, secrets, http, signal, timeoutMs }) {
|
|
252
242
|
const botToken = secrets.botToken || '';
|
|
253
243
|
const chatId = String(settings.chatId || '').trim();
|
package/lib/chat-start.js
CHANGED
|
@@ -24,7 +24,9 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
|
|
|
24
24
|
if (llmModule && typeof llmModule.createUserMessage === 'function') {
|
|
25
25
|
createUserMessage = llmModule.createUserMessage;
|
|
26
26
|
}
|
|
27
|
-
} catch {
|
|
27
|
+
} catch (err) {
|
|
28
|
+
(ctx.logger || console).debug?.('[dsh-cron] @deepseek-ai/dsh-llm createUserMessage unavailable, using identity fallback:', err?.message || err);
|
|
29
|
+
}
|
|
28
30
|
|
|
29
31
|
let mintSessionId = () => `cron-setup-${randomUUID()}`;
|
|
30
32
|
try {
|
|
@@ -33,7 +35,9 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
|
|
|
33
35
|
const sid = mintSessionId();
|
|
34
36
|
mintSessionId = () => sessionModule.SessionId(sid);
|
|
35
37
|
}
|
|
36
|
-
} catch {
|
|
38
|
+
} catch (err) {
|
|
39
|
+
(ctx.logger || console).debug?.('[dsh-cron] @deepseek-ai/dsh-session SessionId unavailable, using string fallback:', err?.message || err);
|
|
40
|
+
}
|
|
37
41
|
|
|
38
42
|
const defaultSel = ctx.get('agentDefaultModel')?.currentSelection?.();
|
|
39
43
|
const provider = body.provider || defaultSel?.provider;
|
package/lib/client.js
CHANGED
|
@@ -883,9 +883,13 @@ window.__ModuleLoader__.load({
|
|
|
883
883
|
.dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary, #9ca3af); font-weight: normal; margin-left: 8px; }
|
|
884
884
|
.dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary, #71717a); }
|
|
885
885
|
|
|
886
|
-
.dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.
|
|
887
|
-
.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; }
|
|
888
|
-
.dsh-cron-modal-scroll { max-width:
|
|
886
|
+
.dsh-cron-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; box-sizing: border-box; overflow-y: auto; }
|
|
887
|
+
.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; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; padding: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); box-sizing: border-box; }
|
|
888
|
+
.dsh-cron-modal-scroll { max-width: 640px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; }
|
|
889
|
+
.dsh-cron-modal::-webkit-scrollbar { width: 6px; }
|
|
890
|
+
.dsh-cron-modal::-webkit-scrollbar-track { background: transparent; }
|
|
891
|
+
.dsh-cron-modal::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l2, #383838); border-radius: 3px; }
|
|
892
|
+
.dsh-cron-modal::-webkit-scrollbar-thumb:hover { background: var(--dsw-alias-label-tertiary, #555); }
|
|
889
893
|
.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; }
|
|
890
894
|
.dsh-cron-section-head:hover { color: var(--dsw-alias-label-primary, #eee); }
|
|
891
895
|
.dsh-cron-section-chevron { transition: transform 0.15s ease; display: inline-block; }
|
|
@@ -899,7 +903,7 @@ window.__ModuleLoader__.load({
|
|
|
899
903
|
.dsh-cron-form-group select { cursor: pointer; }
|
|
900
904
|
.dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
|
|
901
905
|
.dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
902
|
-
.dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; }
|
|
906
|
+
.dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; position: sticky; bottom: -24px; background: var(--dsw-alias-bg-layer-2, #1f1f1f); border-top: 1px solid var(--dsw-alias-border-l2, #2e2e2e); padding-top: 14px; padding-bottom: 4px; margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; z-index: 10; }
|
|
903
907
|
.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; }
|
|
904
908
|
.dsh-cron-btn-primary:hover:not(:disabled) { opacity: 0.88; }
|
|
905
909
|
.dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
@@ -984,6 +988,7 @@ window.__ModuleLoader__.load({
|
|
|
984
988
|
.dsh-cron-channel-grid { grid-template-columns: 1fr; }
|
|
985
989
|
.dsh-cron-filter-panel { grid-template-columns: 1fr; }
|
|
986
990
|
.dsh-cron-modal { padding: 16px; border-radius: 12px; }
|
|
991
|
+
.dsh-cron-modal-foot { bottom: -16px; margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; }
|
|
987
992
|
.dsh-cron-modal-scroll { max-height: 92vh; }
|
|
988
993
|
.dsh-cron-task-item { flex-wrap: wrap; gap: 10px; }
|
|
989
994
|
.dsh-cron-task-actions { margin-left: auto; }
|
|
@@ -1083,6 +1088,7 @@ window.__ModuleLoader__.load({
|
|
|
1083
1088
|
}
|
|
1084
1089
|
}
|
|
1085
1090
|
}
|
|
1091
|
+
console.warn('[dsh-cron] could not open session in workspace:', sessionId);
|
|
1086
1092
|
return false;
|
|
1087
1093
|
}
|
|
1088
1094
|
|
|
@@ -2320,7 +2326,7 @@ window.__ModuleLoader__.load({
|
|
|
2320
2326
|
// 1. Manual creation / Edit modal
|
|
2321
2327
|
manualModalOpen && React.createElement('div', { className: 'dsh-cron-modal-overlay', onClick: () => setManualModalOpen(false) },
|
|
2322
2328
|
React.createElement('div', {
|
|
2323
|
-
className: 'dsh-cron-modal',
|
|
2329
|
+
className: 'dsh-cron-modal dsh-cron-modal-scroll',
|
|
2324
2330
|
style: { maxWidth: '640px' },
|
|
2325
2331
|
role: 'dialog',
|
|
2326
2332
|
'aria-modal': 'true',
|
|
@@ -3311,6 +3317,65 @@ window.__ModuleLoader__.load({
|
|
|
3311
3317
|
const [testInfo, setTestInfo] = React.useState(null);
|
|
3312
3318
|
// Collapsed by default, head is the toggle (canonical card contract, #100)
|
|
3313
3319
|
const [cardOpen, setCardOpen] = React.useState(false);
|
|
3320
|
+
const [updateState, setUpdateState] = React.useState({
|
|
3321
|
+
checking: false,
|
|
3322
|
+
updating: false,
|
|
3323
|
+
currentVersion: '0.2.16',
|
|
3324
|
+
latestVersion: undefined,
|
|
3325
|
+
updateAvailable: false,
|
|
3326
|
+
notice: null,
|
|
3327
|
+
error: null
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
const checkUpdates = async () => {
|
|
3331
|
+
setUpdateState(prev => ({ ...prev, checking: true, error: null, notice: null }));
|
|
3332
|
+
try {
|
|
3333
|
+
const res = await fetch('/api/dsh-cron/update');
|
|
3334
|
+
const data = await res.json();
|
|
3335
|
+
if (data && data.currentVersion) {
|
|
3336
|
+
setUpdateState(prev => ({
|
|
3337
|
+
...prev,
|
|
3338
|
+
checking: false,
|
|
3339
|
+
currentVersion: data.currentVersion,
|
|
3340
|
+
latestVersion: data.latestVersion,
|
|
3341
|
+
updateAvailable: Boolean(data.updateAvailable),
|
|
3342
|
+
error: data.latestCheckFailed ? 'Registry check failed' : null
|
|
3343
|
+
}));
|
|
3344
|
+
} else {
|
|
3345
|
+
setUpdateState(prev => ({ ...prev, checking: false, error: data.error || 'Check failed' }));
|
|
3346
|
+
}
|
|
3347
|
+
} catch (err) {
|
|
3348
|
+
setUpdateState(prev => ({ ...prev, checking: false, error: err.message }));
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
|
|
3352
|
+
const runUpdate = async () => {
|
|
3353
|
+
setUpdateState(prev => ({ ...prev, updating: true, error: null, notice: null }));
|
|
3354
|
+
try {
|
|
3355
|
+
const res = await fetch('/api/dsh-cron/update', {
|
|
3356
|
+
method: 'POST',
|
|
3357
|
+
headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' }
|
|
3358
|
+
});
|
|
3359
|
+
const data = await res.json();
|
|
3360
|
+
if (data && data.updated) {
|
|
3361
|
+
setUpdateState(prev => ({
|
|
3362
|
+
...prev,
|
|
3363
|
+
updating: false,
|
|
3364
|
+
updateAvailable: false,
|
|
3365
|
+
currentVersion: data.updatedVersion || data.latestVersion,
|
|
3366
|
+
notice: T_KEY(t, 'updater.success', { version: data.updatedVersion || data.latestVersion })
|
|
3367
|
+
}));
|
|
3368
|
+
} else {
|
|
3369
|
+
setUpdateState(prev => ({
|
|
3370
|
+
...prev,
|
|
3371
|
+
updating: false,
|
|
3372
|
+
error: data.error || data.message || 'Update failed'
|
|
3373
|
+
}));
|
|
3374
|
+
}
|
|
3375
|
+
} catch (err) {
|
|
3376
|
+
setUpdateState(prev => ({ ...prev, updating: false, error: err.message }));
|
|
3377
|
+
}
|
|
3378
|
+
};
|
|
3314
3379
|
|
|
3315
3380
|
// Settings snapshot status (#102): never render phantom inputs before
|
|
3316
3381
|
// the snapshot arrives; offer a retry when it is unavailable.
|
|
@@ -3516,6 +3581,57 @@ window.__ModuleLoader__.load({
|
|
|
3516
3581
|
'data-type': testInfo.success ? 'success' : 'error',
|
|
3517
3582
|
style: { margin: 0, padding: '8px 12px' }
|
|
3518
3583
|
}, testInfo.message),
|
|
3584
|
+
React.createElement('div', {
|
|
3585
|
+
className: 'dsh-cron-form-group',
|
|
3586
|
+
style: {
|
|
3587
|
+
background: 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.03))',
|
|
3588
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.08))',
|
|
3589
|
+
borderRadius: '8px',
|
|
3590
|
+
padding: '14px',
|
|
3591
|
+
marginBottom: '16px'
|
|
3592
|
+
}
|
|
3593
|
+
},
|
|
3594
|
+
React.createElement('div', {
|
|
3595
|
+
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }
|
|
3596
|
+
},
|
|
3597
|
+
React.createElement('div', { style: { fontWeight: '600', fontSize: '14px' } }, `🚀 ${T_KEY(t, 'updater.title')}`),
|
|
3598
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px' } },
|
|
3599
|
+
React.createElement('button', {
|
|
3600
|
+
type: 'button',
|
|
3601
|
+
className: 'dsh-cron-btn-secondary',
|
|
3602
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
3603
|
+
disabled: updateState.checking || updateState.updating,
|
|
3604
|
+
onClick: checkUpdates
|
|
3605
|
+
}, updateState.checking ? T_KEY(t, 'updater.checking') : T_KEY(t, 'updater.btnCheck')),
|
|
3606
|
+
updateState.updateAvailable ? React.createElement('button', {
|
|
3607
|
+
type: 'button',
|
|
3608
|
+
className: 'dsh-cron-btn-primary',
|
|
3609
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
3610
|
+
disabled: updateState.updating,
|
|
3611
|
+
onClick: runUpdate
|
|
3612
|
+
}, updateState.updating ? T_KEY(t, 'updater.updating') : T_KEY(t, 'updater.btnUpdate', { version: updateState.latestVersion })) : null
|
|
3613
|
+
)
|
|
3614
|
+
),
|
|
3615
|
+
React.createElement('div', {
|
|
3616
|
+
style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #999)', marginBottom: '8px' }
|
|
3617
|
+
}, T_KEY(t, 'updater.desc')),
|
|
3618
|
+
React.createElement('div', {
|
|
3619
|
+
style: { display: 'flex', alignItems: 'center', gap: '12px', fontSize: '13px' }
|
|
3620
|
+
},
|
|
3621
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #aaa)' } },
|
|
3622
|
+
T_KEY(t, 'updater.current', { version: updateState.currentVersion })
|
|
3623
|
+
),
|
|
3624
|
+
updateState.checking ? React.createElement('span', { className: 'dsh-cron-badge dsh-cron-badge-info' }, T_KEY(t, 'updater.checking')) :
|
|
3625
|
+
updateState.updateAvailable ? React.createElement('span', { className: 'dsh-cron-badge dsh-cron-badge-warn' }, T_KEY(t, 'updater.available', { version: updateState.latestVersion })) :
|
|
3626
|
+
updateState.currentVersion && !updateState.error ? React.createElement('span', { className: 'dsh-cron-badge dsh-cron-badge-ok' }, `✓ ${T_KEY(t, 'updater.upToDate')}`) : null
|
|
3627
|
+
),
|
|
3628
|
+
updateState.notice ? React.createElement('div', {
|
|
3629
|
+
style: { marginTop: '8px', color: 'var(--dsw-alias-success, #10b981)', fontSize: '12px' }
|
|
3630
|
+
}, updateState.notice) : null,
|
|
3631
|
+
updateState.error ? React.createElement('div', {
|
|
3632
|
+
style: { marginTop: '8px', color: 'var(--dsw-alias-error, #ef4444)', fontSize: '12px' }
|
|
3633
|
+
}, updateState.error) : null
|
|
3634
|
+
),
|
|
3519
3635
|
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginTop: '4px' } },
|
|
3520
3636
|
React.createElement('button', {
|
|
3521
3637
|
type: 'submit',
|
package/lib/failure-inspector.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { askModel, parseJsonAnswer, resolveAskTarget } from './llm-ask.js';
|
|
14
14
|
|
|
15
15
|
/** Agent-mediated tasks have a prompt worth revising. */
|
|
16
|
-
|
|
16
|
+
const INSPECTABLE_TASK_TYPES = ['llm', 'skill', 'workflow'];
|
|
17
17
|
|
|
18
18
|
export function supportsInspection(task) {
|
|
19
19
|
const type = String((task && task.type) || 'llm');
|
|
@@ -98,10 +98,4 @@ export async function inspectFailure({ task, runInfo, ask, preferredModel, timeo
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
export function makeInspectAsk(ctx) {
|
|
103
|
-
return async ({ prompt, system, preferredModel, maxTokens, timeoutMs, task }) => {
|
|
104
|
-
const target = resolveAskTarget(ctx, task || {}, preferredModel);
|
|
105
|
-
return askModel(ctx, { ...target, prompt, system, maxTokens, timeoutMs });
|
|
106
|
-
};
|
|
107
|
-
}
|
|
101
|
+
// makeInspectAsk removed (#155) — scheduler uses makeAsk(ctx) from lib/llm-ask.js
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { registerPluginUpdater } from './updater.js';
|
|
2
|
+
import { credentialRefStatus } from './secrets.js';
|
|
1
3
|
import { TaskStore } from './store.js';
|
|
2
4
|
import { TaskScheduler, parseScheduleExpression } from './scheduler.js';
|
|
3
5
|
import { SessionRunner } from './runner.js';
|
|
@@ -165,6 +167,15 @@ export function sanitizeSettingsPayload(body) {
|
|
|
165
167
|
delete payload[key];
|
|
166
168
|
}
|
|
167
169
|
}
|
|
170
|
+
for (const key of ['botTokenRef', 'barkKeyRef', 'giteaTokenRef']) {
|
|
171
|
+
if (key in payload && typeof payload[key] === 'string' && payload[key].trim()) {
|
|
172
|
+
const refCheck = credentialRefStatus(payload[key]);
|
|
173
|
+
if (!refCheck.ok) {
|
|
174
|
+
console.warn(`[dsh-cron] invalid credential reference in "${key}": ${refCheck.error}`);
|
|
175
|
+
delete payload[key];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
168
179
|
for (const key of TaskStore.FORBIDDEN_SETTING_KEYS) {
|
|
169
180
|
if (key in payload) {
|
|
170
181
|
console.warn(`[dsh-cron] refusing raw secret "${key}" from the client — use a credential reference`);
|
|
@@ -378,6 +389,13 @@ export function apply(ctx, config) {
|
|
|
378
389
|
// #48: the recipe hub is the single source of the panel's suggestions.
|
|
379
390
|
const recommendations = recipeRecommendations();
|
|
380
391
|
|
|
392
|
+
// Auto-updater route (#147)
|
|
393
|
+
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
394
|
+
endpoint: '/api/dsh-cron/update',
|
|
395
|
+
packageName: '@goodandready/dsh-cron',
|
|
396
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
397
|
+
}), 'dsh-cron: /api/dsh-cron/update');
|
|
398
|
+
|
|
381
399
|
// 1. REST API — collection (list/create) + item actions in one prefix
|
|
382
400
|
// handler per route family; registered WITHOUT the trailing slash (#96)
|
|
383
401
|
ctx.effect(() => ctx.webServer.register({
|
package/lib/recipes.js
CHANGED
|
@@ -34,7 +34,7 @@ export const RECIPE_CATEGORIES = [
|
|
|
34
34
|
* packages, users and permissions, scheduling and the network edge, power state
|
|
35
35
|
* and writes outside the working tree.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
38
38
|
/\brm\b/i,
|
|
39
39
|
/\bunlink\b/i,
|
|
40
40
|
/\bshred\b/i,
|
package/lib/runtimes.js
CHANGED
|
@@ -10,7 +10,7 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
12
|
export const EXTERNAL_RUNTIMES = ['script', 'node', 'python', 'http', 'ssh', 'docker'];
|
|
13
|
-
|
|
13
|
+
const AGENT_RUNTIMES = ['llm', 'skill', 'workflow'];
|
|
14
14
|
export const TASK_TYPES = [...AGENT_RUNTIMES, ...EXTERNAL_RUNTIMES];
|
|
15
15
|
|
|
16
16
|
/** Types that execute code and therefore need the HTTP confirm header. */
|
|
@@ -56,7 +56,7 @@ export function buildNodeInvocation(task, cwd) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Find the Python interpreter, preferring a task virtualenv, then the workspace one. */
|
|
59
|
-
|
|
59
|
+
function detectPython(task, cwd) {
|
|
60
60
|
if (task.pythonPath) return task.pythonPath;
|
|
61
61
|
const candidates = [];
|
|
62
62
|
if (process.env.VIRTUAL_ENV) {
|
package/lib/scheduler.js
CHANGED
|
@@ -6,10 +6,10 @@ import { applySilentRule } from './silent-rule.js';
|
|
|
6
6
|
import { inspectFailure } from './failure-inspector.js';
|
|
7
7
|
|
|
8
8
|
/** Task types that talk to a model and can therefore use a fallback model. */
|
|
9
|
-
|
|
9
|
+
const AGENT_TASK_TYPES = ['llm', 'skill', 'workflow'];
|
|
10
10
|
|
|
11
11
|
/** Sum two usage records, so a fallback attempt is not lost in accounting. */
|
|
12
|
-
|
|
12
|
+
function addUsage(a, b) {
|
|
13
13
|
return {
|
|
14
14
|
inputTokens: (a?.inputTokens || 0) + (b?.inputTokens || 0),
|
|
15
15
|
outputTokens: (a?.outputTokens || 0) + (b?.outputTokens || 0),
|
package/lib/silent-rule.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { askModel, parseJsonAnswer, resolveAskTarget } from './llm-ask.js';
|
|
16
16
|
|
|
17
17
|
/** Task types whose runs produce output a rule can judge. */
|
|
18
|
-
|
|
18
|
+
const SILENT_RULE_TASK_TYPES = ['script', 'node', 'python', 'http'];
|
|
19
19
|
|
|
20
20
|
export function supportsSilentRule(task) {
|
|
21
21
|
return SILENT_RULE_TASK_TYPES.includes(String(task && task.type ? task.type : 'script'));
|
|
@@ -64,6 +64,9 @@ export async function applySilentRule({ task, runInfo, ask, preferredModel, time
|
|
|
64
64
|
if (!task || !task.silentRule || String(task.silentRule).trim() === '') {
|
|
65
65
|
return { skipped: false, reason: '' };
|
|
66
66
|
}
|
|
67
|
+
if (!supportsSilentRule(task)) {
|
|
68
|
+
return { skipped: false, reason: '', note: 'task type does not support silent rule' };
|
|
69
|
+
}
|
|
67
70
|
if ((runInfo && runInfo.status) !== 'success') {
|
|
68
71
|
return { skipped: false, reason: '', note: 'the rule only judges successful runs' };
|
|
69
72
|
}
|
package/lib/telegram.js
CHANGED
|
@@ -79,7 +79,7 @@ export function getDshDefaultTelegramCredentials() {
|
|
|
79
79
|
/**
|
|
80
80
|
* Escape markdown special characters for Telegram legacy markdown.
|
|
81
81
|
*/
|
|
82
|
-
|
|
82
|
+
function escapeMarkdown(str) {
|
|
83
83
|
if (!str) return '';
|
|
84
84
|
return String(str).replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
|
|
85
85
|
}
|