@goodandready/dsh-cron 0.2.18 → 0.2.20
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 +16 -1
- package/README.ru.md +17 -2
- package/README.zh.md +17 -2
- package/lib/api-helpers.js +68 -0
- package/lib/api-import-export.js +145 -0
- package/lib/api-webhook.js +115 -0
- package/lib/api.js +24 -309
- package/lib/best-effort.js +29 -0
- package/lib/channels.js +6 -1
- package/lib/client.js +80 -41
- package/lib/index.js +25 -361
- package/lib/llm-actions.js +9 -6
- package/lib/llm-ask.js +8 -9
- package/lib/recipes.js +3 -1
- package/lib/routes.js +194 -0
- package/lib/runner-external.js +238 -0
- package/lib/runner-worktree.js +55 -0
- package/lib/runner.js +57 -292
- package/lib/runtimes.js +3 -3
- package/lib/scheduler-execution.js +494 -0
- package/lib/scheduler-lifecycle.js +143 -0
- package/lib/scheduler.js +120 -643
- package/lib/secrets.js +6 -7
- package/lib/settings.js +120 -0
- package/lib/store.js +5 -8
- package/lib/task-create.js +60 -0
- package/lib/templates.js +5 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -392,6 +392,15 @@ Developer-facing, no behaviour change. `parseScheduleExpression` was split into
|
|
|
392
392
|
|
|
393
393
|
|
|
394
394
|
|
|
395
|
+
### 31. Quality & Preflight Hardening Pack (v0.2.19, #149, #152, #155, #156, #162)
|
|
396
|
+
- **Zero Empty Catch Blocks (#156)**: Implemented `lib/best-effort.js` matching standard architecture with synchronous/asynchronous error suppression, fallback value handling, and optional context logging. Eliminated all 63 empty catch blocks across runner, scheduler, store, and UI client scripts.
|
|
397
|
+
- **CI Workflows & Local Preflight Gate (#162)**: Added automated CI workflows (`.gitea/workflows/ci.yml` and `.github/workflows/ci.yml`) running test suites and enforcing a strict preflight gate (`scripts/ci-preflight.mjs`) that halts on any syntax error, empty catch, theme hardcoding, or leak attempt.
|
|
398
|
+
- **Theme Token Modernization (#149)**: Replaced all remaining raw `rgba(...)` declarations in UI styles and modals with native `color-mix(in srgb, var(--token) N%, transparent)`.
|
|
399
|
+
- **Plugin Manifest Declarations (#152)**: Updated `dsh.client.inject` in `package.json` to declare full package dependencies (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`).
|
|
400
|
+
- **Production Export Integration (#155)**: Connected test-only exports (`findDestructiveRecipe` in `listRecipes`, `shouldNotifyTask` in `shouldSendToChannel`, and `TEMPLATE_VARIABLES` in `buildTemplateVars`) into active production pipelines.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
395
404
|
### 30. Updater Localization in English & Chinese (v0.2.18, #160)
|
|
396
405
|
- **Settings Card Updater Localization (#160)**: Added native translations for all 10 in-app updater keys (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`) into English (`en`) and Chinese (`zh`) dictionaries in `lib/client-src/10-locales.js`. In accordance with DSH core plugin architecture, Russian localization is maintained externally via `dsh-russian-lang`.
|
|
397
406
|
|
|
@@ -529,7 +538,7 @@ All endpoints are served by the DSH web server under `/dsh-cron/`. Read endpoint
|
|
|
529
538
|
|
|
530
539
|
---
|
|
531
540
|
|
|
532
|
-
## 🧪 Testing
|
|
541
|
+
## 🧪 Testing & Preflight Verification
|
|
533
542
|
|
|
534
543
|
Run the automated test suite covering schedule parsing, the scheduler engine, atomic storage, HTTP helpers, notifications and tool contracts:
|
|
535
544
|
|
|
@@ -537,6 +546,12 @@ Run the automated test suite covering schedule parsing, the scheduler engine, at
|
|
|
537
546
|
npm test
|
|
538
547
|
```
|
|
539
548
|
|
|
549
|
+
Run the local preflight gate to verify syntax, zero empty catch blocks, theme token compliance, package boundaries and leak protection:
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
node scripts/ci-preflight.mjs
|
|
553
|
+
```
|
|
554
|
+
|
|
540
555
|
---
|
|
541
556
|
|
|
542
557
|
## 📄 License
|
package/README.ru.md
CHANGED
|
@@ -268,6 +268,15 @@ cron_create_task({
|
|
|
268
268
|
|
|
269
269
|
|
|
270
270
|
|
|
271
|
+
### 31. Пакет ужесточения стандартов качества и CI (v0.2.19, #149, #152, #155, #156, #162)
|
|
272
|
+
- **Устранение пустых catch (#156)**: Реализован модуль `lib/best-effort.js` по единому стандарту с безопасным выполнением синхронных/асинхронных операций и опциональным логированием. Все 63 пустых catch-блока в планировщике, раннере, хранилище и клиентских фрагментах устранены.
|
|
273
|
+
- **Автоматический CI и локальный гейт Preflight (#162)**: Добавлены workflows непрерывной интеграции (`.gitea/workflows/ci.yml` и `.github/workflows/ci.yml`), проверяющие синтаксис, тесты и запускающие механический аудит `scripts/ci-preflight.mjs` с блокировкой любых отклонений.
|
|
274
|
+
- **Модернизация токенов темы (#149)**: Оставшиеся цвета `rgba(...)` в модальных окнах и карточках переведены на современный стандарт `color-mix(in srgb, var(--токен) N%, transparent)`.
|
|
275
|
+
- **Декларация зависимостей в манифесте (#152)**: В `package.json` поле `dsh.client.inject` переведено на полные имена пакетов Cordis (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`).
|
|
276
|
+
- **Активация экспортов в рабочем коде (#155)**: Функции `findDestructiveRecipe`, `shouldNotifyTask` и переменные `TEMPLATE_VARIABLES` подключены в активные цепочки работы с рецептами, роутинга уведомлений и рендеринга шаблонов.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
271
280
|
### 30. Локализация автообновления на английский и китайский языки (v0.2.18, #160)
|
|
272
281
|
- **Локализация карточки обновления (#160)**: Добавлены нативные переводы для всех 10 ключей модуля автообновления (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`) в словари английского (`en`) и китайского (`zh`) языков в `lib/client-src/10-locales.js`. В соответствии со стандартами ядра DSH плагин поставляется с EN и ZH словарями, а русская локализация обслуживается через системный плагин `dsh-russian-lang`.
|
|
273
282
|
|
|
@@ -526,13 +535,19 @@ bash deploy.sh verify [exact-version]
|
|
|
526
535
|
|
|
527
536
|
---
|
|
528
537
|
|
|
529
|
-
## 🧪 Тестирование
|
|
538
|
+
## 🧪 Тестирование и локальная проверка (Preflight)
|
|
539
|
+
|
|
540
|
+
Запуск автоматического набора тестов (разбор расписаний, движок планировщика, атомарное хранилище, HTTP-хелперы, уведомления и контракт инструментов):
|
|
530
541
|
|
|
531
542
|
```bash
|
|
532
543
|
npm test
|
|
533
544
|
```
|
|
534
545
|
|
|
535
|
-
|
|
546
|
+
Локальный запуск гейта стандартов качества (синтаксис, отсутствие пустых catch, соответствие токенам тем оформления, чистота состава npm-пакета и защита от утечек):
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
node scripts/ci-preflight.mjs
|
|
550
|
+
```
|
|
536
551
|
|
|
537
552
|
---
|
|
538
553
|
|
package/README.zh.md
CHANGED
|
@@ -391,6 +391,15 @@ bash deploy.sh verify [exact-version]
|
|
|
391
391
|
|
|
392
392
|
|
|
393
393
|
|
|
394
|
+
### 31. 质量加固与 CI 预检标准包 (v0.2.19, #149, #152, #155, #156, #162)
|
|
395
|
+
- **彻底消除空 catch 块 (#156)**:引入符合标准规范的 `lib/best-effort.js` 模块,全面支持同步/异步安全调用、回退返回值和可选日志记录,清除了 runner、scheduler、store 及客户端中的所有 63 处空 catch 块。
|
|
396
|
+
- **CI 工作流与本地预检门禁 (#162)**:新增自动化 CI 工作流(`.gitea/workflows/ci.yml` 与 `.github/workflows/ci.yml`),集成本地预检门禁脚本 `scripts/ci-preflight.mjs`,在出现语法错误、空 catch、颜色硬编码或信息泄露时自动阻断。
|
|
397
|
+
- **主题设计令牌现代化 (#149)**:将模态对话框与设置卡片中遗留的 `rgba(...)` 全部重构为原生的 `color-mix(in srgb, var(--token) N%, transparent)`。
|
|
398
|
+
- **插件清单注入声明规范化 (#152)**:在 `package.json` 的 `dsh.client.inject` 中统一声明完整的 Cordis 包名(`@deepseek-ai/dsh-client-locale`、`@deepseek-ai/dsh-client-ui-slots`)。
|
|
399
|
+
- **生产管线导出连接 (#155)**:将此前仅在测试中调用的导出(`findDestructiveRecipe`、`shouldNotifyTask` 和 `TEMPLATE_VARIABLES`)全面接入配方过滤、通知分发及模板生成核心逻辑。
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
394
403
|
### 30. 自更新模块英文与中文多语言支持 (v0.2.18, #160)
|
|
395
404
|
- **设置面板自更新多语言 (#160)**:在 `lib/client-src/10-locales.js` 的英文 (`en`) 与中文 (`zh`) 字典中补全了全部 10 个自更新键值 (`updater.title`, `updater.btnCheck`, `updater.checking`, `updater.btnUpdate`, `updater.updating`, `updater.desc`, `updater.current`, `updater.available`, `updater.upToDate`, `updater.success`)。遵循 DSH 插件规范,插件核心内置英文与中文,俄语多语言由 `dsh-russian-lang` 统一扩展。
|
|
396
405
|
|
|
@@ -526,13 +535,19 @@ dsh-cron:
|
|
|
526
535
|
|
|
527
536
|
---
|
|
528
537
|
|
|
529
|
-
## 🧪
|
|
538
|
+
## 🧪 测试与预检门禁 (Preflight)
|
|
539
|
+
|
|
540
|
+
运行全套自动化测试(调度表达式解析、调度器引擎、原子存储、HTTP 辅助函数、通知与工具契约):
|
|
530
541
|
|
|
531
542
|
```bash
|
|
532
543
|
npm test
|
|
533
544
|
```
|
|
534
545
|
|
|
535
|
-
|
|
546
|
+
本地执行质量预检门禁(语法检查、零空 catch 块、主题设计规范、npm 发布归档合规性及防泄漏检查):
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
node scripts/ci-preflight.mjs
|
|
550
|
+
```
|
|
536
551
|
|
|
537
552
|
---
|
|
538
553
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { previewSchedule } from './scheduler.js';
|
|
2
|
+
import { sendJson, readBody, rejectCrossOrigin } from './http-utils.js';
|
|
3
|
+
|
|
4
|
+
const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
|
|
5
|
+
|
|
6
|
+
export function handleHeartbeatPing({ store, req, res, taskId }) {
|
|
7
|
+
const task = store.get(taskId);
|
|
8
|
+
if (!task) {
|
|
9
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const result = typeof store.recordHeartbeat === 'function' ? store.recordHeartbeat(taskId) : null;
|
|
13
|
+
if (!result) {
|
|
14
|
+
sendJson(res, 500, { ok: false, error: 'Could not record heartbeat' });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
sendJson(res, 200, result);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleSchedulePreview({ req, res, url }) {
|
|
21
|
+
let schedule = '';
|
|
22
|
+
let timezone = '';
|
|
23
|
+
let count = 5;
|
|
24
|
+
|
|
25
|
+
if (req.method === 'POST') {
|
|
26
|
+
const { body, error } = await readBody(req, res);
|
|
27
|
+
if (error) return;
|
|
28
|
+
schedule = body.schedule || '';
|
|
29
|
+
timezone = body.timezone || '';
|
|
30
|
+
count = body.count || 5;
|
|
31
|
+
} else if (req.method === 'GET') {
|
|
32
|
+
schedule = url.searchParams.get('schedule') || '';
|
|
33
|
+
timezone = url.searchParams.get('timezone') || '';
|
|
34
|
+
count = parseInt(url.searchParams.get('count') || '5', 10);
|
|
35
|
+
} else {
|
|
36
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!schedule) {
|
|
41
|
+
sendJson(res, 400, { ok: false, error: 'Field "schedule" is required' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const preview = previewSchedule(schedule, { timezone, count });
|
|
47
|
+
sendJson(res, 200, { ok: true, ...preview });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
sendJson(res, 400, { ok: false, error: err.message });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function handleDryRunTask({ store, scheduler, req, res, id }) {
|
|
54
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
55
|
+
const task = store.get(id);
|
|
56
|
+
if (!task) {
|
|
57
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const outcome = await scheduler.runTask(id, { dryRun: true });
|
|
63
|
+
sendJson(res, 200, outcome || { ok: true, status: 'completed' });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
buildTaskExport,
|
|
4
|
+
validateImportDocument,
|
|
5
|
+
planImport,
|
|
6
|
+
hasCodeExecutingTask,
|
|
7
|
+
IMPORT_STRATEGIES,
|
|
8
|
+
} from './task-transfer.js';
|
|
9
|
+
import { unknownChannelIds } from './channels.js';
|
|
10
|
+
import {
|
|
11
|
+
sendJson,
|
|
12
|
+
readBody,
|
|
13
|
+
SCRIPT_CONFIRM_HEADER,
|
|
14
|
+
} from './http-utils.js';
|
|
15
|
+
import { MANAGED_BY_CONFIG } from './config-jobs.js';
|
|
16
|
+
|
|
17
|
+
export function handleTaskExport({ store, res }) {
|
|
18
|
+
const doc = buildTaskExport(store.list({ status: 'all' }));
|
|
19
|
+
sendJson(res, 200, { ok: true, document: doc, count: doc.tasks.length });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function handleTaskImport({ store, scheduler, req, res }) {
|
|
23
|
+
const { body, error } = await readBody(req, res);
|
|
24
|
+
if (error) return;
|
|
25
|
+
const strategy = IMPORT_STRATEGIES.includes(body && body.strategy) ? body.strategy : 'skip';
|
|
26
|
+
const checked = validateImportDocument(body && body.document);
|
|
27
|
+
if (!checked.ok) {
|
|
28
|
+
sendJson(res, 400, { ok: false, error: checked.error });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// The import file is external input: importing code-executing tasks needs
|
|
32
|
+
// the same explicit confirmation that creating one over HTTP needs,
|
|
33
|
+
// otherwise a hand-edited file would bypass that gate (#86).
|
|
34
|
+
if (hasCodeExecutingTask(checked.tasks) && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
35
|
+
sendJson(res, 403, {
|
|
36
|
+
ok: false,
|
|
37
|
+
error: `Importing code-executing tasks over HTTP requires the ${SCRIPT_CONFIRM_HEADER}: script header`,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// #121: an imported file may name a channel this build no longer has. The
|
|
42
|
+
// import stays tolerant by design (#42), but the loss is reported instead of
|
|
43
|
+
// being invisible.
|
|
44
|
+
const rawTasks = Array.isArray(body.document && body.document.tasks) ? body.document.tasks : [];
|
|
45
|
+
const droppedChannels = [];
|
|
46
|
+
for (const item of rawTasks) {
|
|
47
|
+
for (const id of unknownChannelIds(item.channels)) {
|
|
48
|
+
if (!droppedChannels.includes(id)) droppedChannels.push(id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (droppedChannels.length) {
|
|
52
|
+
console.warn('[dsh-cron] import: unknown channel ids were dropped: ' + droppedChannels.join(', '));
|
|
53
|
+
}
|
|
54
|
+
const existingIds = store.list({ status: 'all' }).map((x) => x.id);
|
|
55
|
+
const plan = planImport(checked.tasks, existingIds, strategy);
|
|
56
|
+
// Replacing a task the config owns would be undone at the next start, and
|
|
57
|
+
// the id could then hold two different tasks (#50).
|
|
58
|
+
const configConflicts = plan.replace
|
|
59
|
+
.map((item) => item.id)
|
|
60
|
+
.filter((taskId) => {
|
|
61
|
+
const stored = store.get(taskId);
|
|
62
|
+
return stored && stored.managedBy === MANAGED_BY_CONFIG;
|
|
63
|
+
});
|
|
64
|
+
if (configConflicts.length) {
|
|
65
|
+
sendJson(res, 409, {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: 'These tasks are declared in the profile config and cannot be replaced by an import: ' + configConflicts.join(', '),
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const summary = { add: plan.add.length, replace: plan.replace.length, skip: plan.skip.length };
|
|
72
|
+
if (body && body.dryRun) {
|
|
73
|
+
sendJson(res, 200, {
|
|
74
|
+
ok: true,
|
|
75
|
+
dryRun: true,
|
|
76
|
+
strategy: plan.strategy,
|
|
77
|
+
summary,
|
|
78
|
+
codeExecuting: hasCodeExecutingTask(plan.add.concat(plan.replace)),
|
|
79
|
+
unknownChannels: droppedChannels,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const applied = applyImportPlan({ store, scheduler, plan });
|
|
84
|
+
if (applied.error) {
|
|
85
|
+
sendJson(res, 500, { ok: false, error: applied.error });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
sendJson(res, 200, {
|
|
89
|
+
ok: true,
|
|
90
|
+
imported: applied.imported.length,
|
|
91
|
+
summary,
|
|
92
|
+
unknownChannels: droppedChannels,
|
|
93
|
+
tasks: applied.imported,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Write an import plan. Every imported task is paused (the file's status is
|
|
99
|
+
* ignored) and every touched id is disarmed explicitly, because writing
|
|
100
|
+
* "paused" does not stop an already-armed croner job. A failed write rolls the
|
|
101
|
+
* store back so a partial import cannot survive.
|
|
102
|
+
*/
|
|
103
|
+
export function applyImportPlan({ store, scheduler, plan }) {
|
|
104
|
+
const imported = [];
|
|
105
|
+
const rollback = [];
|
|
106
|
+
const disarm = [];
|
|
107
|
+
try {
|
|
108
|
+
for (const task of plan.add) {
|
|
109
|
+
const before = store.get(task.id);
|
|
110
|
+
const created = store.set({ ...task, id: task.id || randomUUID(), status: 'paused', updatedAt: Date.now() });
|
|
111
|
+
imported.push(created);
|
|
112
|
+
rollback.push({ id: created.id, previous: before || null });
|
|
113
|
+
disarm.push(created.id);
|
|
114
|
+
}
|
|
115
|
+
for (const task of plan.replace) {
|
|
116
|
+
const before = store.get(task.id);
|
|
117
|
+
if (!before) continue;
|
|
118
|
+
const replaced = store.set({ ...task, id: task.id, status: 'paused', updatedAt: Date.now() });
|
|
119
|
+
imported.push(replaced);
|
|
120
|
+
rollback.push({ id: replaced.id, previous: before });
|
|
121
|
+
disarm.push(replaced.id);
|
|
122
|
+
}
|
|
123
|
+
} catch (writeErr) {
|
|
124
|
+
for (const entry of rollback.reverse()) {
|
|
125
|
+
try {
|
|
126
|
+
if (entry.previous) store.set(entry.previous);
|
|
127
|
+
else store.delete(entry.id);
|
|
128
|
+
} catch (rollbackErr) {
|
|
129
|
+
console.warn(`[dsh-cron] import rollback failed for task "${entry.id}":`, rollbackErr?.message || rollbackErr);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { imported: [], error: `Import failed and was rolled back: ${writeErr.message}` };
|
|
133
|
+
}
|
|
134
|
+
// Disarm outside the write transaction: pauseTask is idempotent and a
|
|
135
|
+
// failure here must not undo a successful import.
|
|
136
|
+
for (const id of disarm) {
|
|
137
|
+
try {
|
|
138
|
+
scheduler.pauseTask(id);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error('[dsh-cron] import: could not pause task', id + ':', err.message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { imported };
|
|
144
|
+
}
|
|
145
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { sendJson, readBody } from './http-utils.js';
|
|
2
|
+
import { answerTelegramCallbackQuery, sendTelegramMessage, getDshDefaultTelegramCredentials } from './telegram.js';
|
|
3
|
+
|
|
4
|
+
const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
|
|
5
|
+
|
|
6
|
+
export async function handleTelegramWebhook({ store, scheduler, req, res }) {
|
|
7
|
+
if (req.method !== 'POST') {
|
|
8
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const { body, error } = await readBody(req, res);
|
|
12
|
+
if (error || !body) return;
|
|
13
|
+
|
|
14
|
+
const callbackQuery = body.callback_query;
|
|
15
|
+
if (!callbackQuery) {
|
|
16
|
+
sendJson(res, 200, { ok: true });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const queryId = callbackQuery.id;
|
|
21
|
+
const data = String(callbackQuery.data || '');
|
|
22
|
+
const fromId = String(callbackQuery.from?.id || '');
|
|
23
|
+
const chatId = String(callbackQuery.message?.chat?.id || fromId);
|
|
24
|
+
|
|
25
|
+
const settings = typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
26
|
+
const defaults = getDshDefaultTelegramCredentials();
|
|
27
|
+
const botToken = settings.botToken || defaults.botToken;
|
|
28
|
+
const allowedChatId = String(settings.chatId || defaults.chatId || '').trim();
|
|
29
|
+
|
|
30
|
+
if (allowedChatId && chatId !== allowedChatId && fromId !== allowedChatId) {
|
|
31
|
+
console.warn(`[dsh-cron] unauthorized telegram callback from chat ${chatId} / user ${fromId}`);
|
|
32
|
+
if (botToken) {
|
|
33
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Unauthorized', showAlert: true });
|
|
34
|
+
}
|
|
35
|
+
sendJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const parts = data.split(':');
|
|
40
|
+
if (parts[0] !== 'cron' || parts.length < 3) {
|
|
41
|
+
if (botToken) {
|
|
42
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Unknown action' });
|
|
43
|
+
}
|
|
44
|
+
sendJson(res, 200, { ok: true, ignored: true });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const verb = parts[1];
|
|
49
|
+
const taskId = parts.slice(2).join(':');
|
|
50
|
+
const task = store.get(taskId);
|
|
51
|
+
|
|
52
|
+
if (!task) {
|
|
53
|
+
if (botToken) {
|
|
54
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Task not found', showAlert: true });
|
|
55
|
+
}
|
|
56
|
+
sendJson(res, 200, { ok: false, error: 'Task not found' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (verb === 'run') {
|
|
61
|
+
try {
|
|
62
|
+
await scheduler.triggerManualRun(taskId);
|
|
63
|
+
if (botToken) {
|
|
64
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `🚀 Run triggered: ${task.title}` });
|
|
65
|
+
}
|
|
66
|
+
} catch (runErr) {
|
|
67
|
+
if (botToken) {
|
|
68
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `Run error: ${runErr.message}`, showAlert: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else if (verb === 'pause') {
|
|
72
|
+
const isPaused = task.status === 'paused';
|
|
73
|
+
const newStatus = isPaused ? 'active' : 'paused';
|
|
74
|
+
if (typeof store.update === 'function') {
|
|
75
|
+
store.update(taskId, { status: newStatus });
|
|
76
|
+
} else {
|
|
77
|
+
store.set({ ...task, status: newStatus });
|
|
78
|
+
}
|
|
79
|
+
if (isPaused) {
|
|
80
|
+
scheduler.resume(taskId);
|
|
81
|
+
if (botToken) {
|
|
82
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `▶️ Resumed: ${task.title}` });
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
scheduler.pause(taskId);
|
|
86
|
+
if (botToken) {
|
|
87
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `⏸ Paused: ${task.title}` });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else if (verb === 'log') {
|
|
91
|
+
const history = store.getHistory(taskId, 1);
|
|
92
|
+
const lastRun = history && history[0];
|
|
93
|
+
if (!lastRun) {
|
|
94
|
+
if (botToken) {
|
|
95
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `No runs recorded for ${task.title}`, showAlert: true });
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
const summary = `[${task.title}] Last run: ${lastRun.status} (${lastRun.durationMs}ms)\n${lastRun.output || lastRun.error || 'Empty output'}`;
|
|
99
|
+
if (botToken) {
|
|
100
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: summary.slice(0, 190) });
|
|
101
|
+
if (summary.length > 190 && chatId) {
|
|
102
|
+
await sendTelegramMessage({
|
|
103
|
+
botToken,
|
|
104
|
+
chatId,
|
|
105
|
+
text: `*Full Output for ${task.title}:*\n\`\`\`\n${summary.slice(0, 3000)}\n\`\`\``,
|
|
106
|
+
parseMode: 'Markdown',
|
|
107
|
+
}).catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sendJson(res, 200, { ok: true, verb, taskId });
|
|
114
|
+
}
|
|
115
|
+
|