@goodandready/dsh-cron 0.2.9 → 0.2.11
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 +27 -2
- package/README.ru.md +481 -0
- package/README.zh.md +481 -0
- package/docs/README.ru.md +34 -1
- package/docs/README.zh.md +34 -1
- package/lib/api.js +224 -2
- package/lib/channels.js +3 -2
- package/lib/client.js +594 -3
- package/lib/index.js +21 -2
- package/lib/llm-actions.js +141 -0
- package/lib/metrics.js +24 -1
- package/lib/runner.js +24 -11
- package/lib/scheduler.js +302 -13
- package/lib/store.js +90 -0
- package/lib/task-transfer.js +10 -0
- package/lib/telegram.js +43 -1
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { randomUUID, createHash } from 'node:crypto';
|
|
9
|
-
import { parseScheduleExpression } from './scheduler.js';
|
|
9
|
+
import { parseScheduleExpression, previewSchedule } from './scheduler.js';
|
|
10
10
|
import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
|
|
11
11
|
import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
|
|
12
12
|
import { applyTaskPatch } from './task-patch.js';
|
|
13
13
|
import { MANAGED_BY_CONFIG, configOwnedMessage } from './config-jobs.js';
|
|
14
14
|
import { sendJson, rejectCrossOrigin, readBody, pickPatchableFields, SCRIPT_CONFIRM_HEADER } from './http-utils.js';
|
|
15
|
+
import { answerTelegramCallbackQuery, sendTelegramMessage, getDshDefaultTelegramCredentials } from './telegram.js';
|
|
15
16
|
import {
|
|
16
17
|
validateTaskType,
|
|
17
18
|
buildDuplicateTask,
|
|
@@ -54,6 +55,21 @@ export function createCronApiHandler(store, scheduler, collection) {
|
|
|
54
55
|
const id = parts[2];
|
|
55
56
|
const action = parts[3];
|
|
56
57
|
|
|
58
|
+
if (scope === 'telegram' && id === 'webhook') {
|
|
59
|
+
await handleTelegramWebhook({ store, scheduler, req, res });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (scope === 'heartbeat' && id) {
|
|
64
|
+
handleHeartbeatPing({ store, req, res, taskId: id });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (scope === 'schedule' && id === 'preview') {
|
|
69
|
+
await handleSchedulePreview({ req, res, url });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
if (!id) {
|
|
58
74
|
await handleTaskCollection({ store, scheduler, collection, req, res, url, scope });
|
|
59
75
|
return;
|
|
@@ -346,6 +362,31 @@ function applyImportPlan({ store, scheduler, plan }) {
|
|
|
346
362
|
// ------------------------------------------------------------ single task
|
|
347
363
|
|
|
348
364
|
async function handleTaskItem({ store, scheduler, req, res, url, id, action }) {
|
|
365
|
+
if ((req.method === 'GET' || req.method === 'POST') && action === 'heartbeat') {
|
|
366
|
+
handleHeartbeatPing({ store, req, res, taskId: id });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (req.method === 'POST' && action === 'dry-run') {
|
|
371
|
+
await handleDryRunTask({ store, scheduler, req, res, id });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (req.method === 'GET' && action === 'archive') {
|
|
376
|
+
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
377
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
|
378
|
+
const search = url.searchParams.get('search') || '';
|
|
379
|
+
const result = typeof store.getArchivedRuns === 'function'
|
|
380
|
+
? store.getArchivedRuns(id, { limit, offset, search })
|
|
381
|
+
: { runs: [], total: 0 };
|
|
382
|
+
sendJson(res, 200, { ok: true, taskId: id, ...result });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (req.method === 'GET' && action === 'stats') {
|
|
386
|
+
const stats = typeof store.getTaskStats === 'function' ? store.getTaskStats(id) : null;
|
|
387
|
+
sendJson(res, 200, { ok: true, taskId: id, stats });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
349
390
|
if (req.method === 'GET' && (action === 'history' || !action)) {
|
|
350
391
|
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
351
392
|
sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
|
|
@@ -511,7 +552,15 @@ function mergeExecutionFields(current, body, parsed) {
|
|
|
511
552
|
* tasks and the config-owned refusals are the same code, not a second copy.
|
|
512
553
|
*/
|
|
513
554
|
export async function handleExternalTaskRequest({ store, scheduler, req, res, url }) {
|
|
514
|
-
const parts = url.pathname.split('/').filter(Boolean); // dsh-cron, api, tasks, :id?, :action?
|
|
555
|
+
const parts = url.pathname.split('/').filter(Boolean); // dsh-cron, api, [tasks|heartbeat|schedule], :id?, :action?
|
|
556
|
+
if (parts[2] === 'heartbeat' && parts[3]) {
|
|
557
|
+
handleHeartbeatPing({ store, req, res, taskId: parts[3] });
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (parts[2] === 'schedule' && parts[3] === 'preview') {
|
|
561
|
+
await handleSchedulePreview({ req, res, url });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
515
564
|
const id = parts[3];
|
|
516
565
|
const action = parts[4];
|
|
517
566
|
|
|
@@ -550,3 +599,176 @@ export async function handleExternalTaskRequest({ store, scheduler, req, res, ur
|
|
|
550
599
|
}
|
|
551
600
|
sendJson(res, 405, NOT_ALLOWED);
|
|
552
601
|
}
|
|
602
|
+
|
|
603
|
+
export async function handleTelegramWebhook({ store, scheduler, req, res }) {
|
|
604
|
+
if (req.method !== 'POST') {
|
|
605
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const { body, error } = await readBody(req, res);
|
|
609
|
+
if (error || !body) return;
|
|
610
|
+
|
|
611
|
+
const callbackQuery = body.callback_query;
|
|
612
|
+
if (!callbackQuery) {
|
|
613
|
+
sendJson(res, 200, { ok: true });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const queryId = callbackQuery.id;
|
|
618
|
+
const data = String(callbackQuery.data || '');
|
|
619
|
+
const fromId = String(callbackQuery.from?.id || '');
|
|
620
|
+
const chatId = String(callbackQuery.message?.chat?.id || fromId);
|
|
621
|
+
|
|
622
|
+
const settings = typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
623
|
+
const defaults = getDshDefaultTelegramCredentials();
|
|
624
|
+
const botToken = settings.botToken || defaults.botToken;
|
|
625
|
+
const allowedChatId = String(settings.chatId || defaults.chatId || '').trim();
|
|
626
|
+
|
|
627
|
+
if (allowedChatId && chatId !== allowedChatId && fromId !== allowedChatId) {
|
|
628
|
+
console.warn(`[dsh-cron] unauthorized telegram callback from chat ${chatId} / user ${fromId}`);
|
|
629
|
+
if (botToken) {
|
|
630
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Unauthorized', showAlert: true });
|
|
631
|
+
}
|
|
632
|
+
sendJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const parts = data.split(':');
|
|
637
|
+
if (parts[0] !== 'cron' || parts.length < 3) {
|
|
638
|
+
if (botToken) {
|
|
639
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Unknown action' });
|
|
640
|
+
}
|
|
641
|
+
sendJson(res, 200, { ok: true, ignored: true });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const verb = parts[1];
|
|
646
|
+
const taskId = parts.slice(2).join(':');
|
|
647
|
+
const task = store.get(taskId);
|
|
648
|
+
|
|
649
|
+
if (!task) {
|
|
650
|
+
if (botToken) {
|
|
651
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: 'Task not found', showAlert: true });
|
|
652
|
+
}
|
|
653
|
+
sendJson(res, 200, { ok: false, error: 'Task not found' });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (verb === 'run') {
|
|
658
|
+
try {
|
|
659
|
+
await scheduler.triggerManualRun(taskId);
|
|
660
|
+
if (botToken) {
|
|
661
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `🚀 Run triggered: ${task.title}` });
|
|
662
|
+
}
|
|
663
|
+
} catch (runErr) {
|
|
664
|
+
if (botToken) {
|
|
665
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `Run error: ${runErr.message}`, showAlert: true });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} else if (verb === 'pause') {
|
|
669
|
+
const isPaused = task.status === 'paused';
|
|
670
|
+
const newStatus = isPaused ? 'active' : 'paused';
|
|
671
|
+
if (typeof store.update === 'function') {
|
|
672
|
+
store.update(taskId, { status: newStatus });
|
|
673
|
+
} else {
|
|
674
|
+
store.set({ ...task, status: newStatus });
|
|
675
|
+
}
|
|
676
|
+
if (isPaused) {
|
|
677
|
+
scheduler.resume(taskId);
|
|
678
|
+
if (botToken) {
|
|
679
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `▶️ Resumed: ${task.title}` });
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
scheduler.pause(taskId);
|
|
683
|
+
if (botToken) {
|
|
684
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `⏸ Paused: ${task.title}` });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} else if (verb === 'log') {
|
|
688
|
+
const history = store.getHistory(taskId, 1);
|
|
689
|
+
const lastRun = history && history[0];
|
|
690
|
+
if (!lastRun) {
|
|
691
|
+
if (botToken) {
|
|
692
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: `No runs recorded for ${task.title}`, showAlert: true });
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
const summary = `[${task.title}] Last run: ${lastRun.status} (${lastRun.durationMs}ms)\n${lastRun.output || lastRun.error || 'Empty output'}`;
|
|
696
|
+
if (botToken) {
|
|
697
|
+
await answerTelegramCallbackQuery({ botToken, callbackQueryId: queryId, text: summary.slice(0, 190) });
|
|
698
|
+
if (summary.length > 190 && chatId) {
|
|
699
|
+
await sendTelegramMessage({
|
|
700
|
+
botToken,
|
|
701
|
+
chatId,
|
|
702
|
+
text: `*Full Output for ${task.title}:*\n\`\`\`\n${summary.slice(0, 3000)}\n\`\`\``,
|
|
703
|
+
parseMode: 'Markdown',
|
|
704
|
+
}).catch(() => {});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
sendJson(res, 200, { ok: true, verb, taskId });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function handleHeartbeatPing({ store, req, res, taskId }) {
|
|
714
|
+
const task = store.get(taskId);
|
|
715
|
+
if (!task) {
|
|
716
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const result = typeof store.recordHeartbeat === 'function' ? store.recordHeartbeat(taskId) : null;
|
|
720
|
+
if (!result) {
|
|
721
|
+
sendJson(res, 500, { ok: false, error: 'Could not record heartbeat' });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
sendJson(res, 200, result);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export async function handleSchedulePreview({ req, res, url }) {
|
|
728
|
+
let schedule = '';
|
|
729
|
+
let timezone = '';
|
|
730
|
+
let count = 5;
|
|
731
|
+
|
|
732
|
+
if (req.method === 'POST') {
|
|
733
|
+
const { body, error } = await readBody(req, res);
|
|
734
|
+
if (error) return;
|
|
735
|
+
schedule = body.schedule || '';
|
|
736
|
+
timezone = body.timezone || '';
|
|
737
|
+
count = body.count || 5;
|
|
738
|
+
} else if (req.method === 'GET') {
|
|
739
|
+
schedule = url.searchParams.get('schedule') || '';
|
|
740
|
+
timezone = url.searchParams.get('timezone') || '';
|
|
741
|
+
count = parseInt(url.searchParams.get('count') || '5', 10);
|
|
742
|
+
} else {
|
|
743
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (!schedule) {
|
|
748
|
+
sendJson(res, 400, { ok: false, error: 'Field "schedule" is required' });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
try {
|
|
753
|
+
const preview = previewSchedule(schedule, { timezone, count });
|
|
754
|
+
sendJson(res, 200, { ok: true, ...preview });
|
|
755
|
+
} catch (err) {
|
|
756
|
+
sendJson(res, 400, { ok: false, error: err.message });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export async function handleDryRunTask({ store, scheduler, req, res, id }) {
|
|
761
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
762
|
+
const task = store.get(id);
|
|
763
|
+
if (!task) {
|
|
764
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const outcome = await scheduler.runTask(id, { dryRun: true });
|
|
770
|
+
sendJson(res, 200, outcome || { ok: true, status: 'completed' });
|
|
771
|
+
} catch (err) {
|
|
772
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
773
|
+
}
|
|
774
|
+
}
|
package/lib/channels.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* collected so one broken channel cannot hide another.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { formatTaskTelegramMessage, sendTelegramMessage } from './telegram.js';
|
|
10
|
+
import { formatTaskTelegramMessage, sendTelegramMessage, createTelegramTaskKeyboard } from './telegram.js';
|
|
11
11
|
import { createKanbanCard, shouldCreateKanbanCard } from './integrations.js';
|
|
12
12
|
import { resolveTemplateText, truncateText } from './templates.js';
|
|
13
13
|
|
|
@@ -256,7 +256,8 @@ export const CHANNEL_HANDLERS = {
|
|
|
256
256
|
? messageTextFor('telegram', task, runInfo, settings)
|
|
257
257
|
: formatTaskTelegramMessage(task, runInfo);
|
|
258
258
|
try {
|
|
259
|
-
|
|
259
|
+
const replyMarkup = createTelegramTaskKeyboard(task, runInfo);
|
|
260
|
+
await sendTelegramMessage({ botToken, chatId, text, replyMarkup, fetchFn: http, signal });
|
|
260
261
|
} catch (err) {
|
|
261
262
|
if (isAbort(err)) throw new Error(`telegram: timed out after ${timeoutMs} ms`);
|
|
262
263
|
throw err;
|