@goodandready/dsh-cron 0.1.24 → 0.2.0
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 +117 -82
- package/docs/README.ru.md +133 -85
- package/docs/README.zh.md +140 -82
- package/docs/design/DESIGN.md +44 -36
- package/lib/chat-start.js +2 -2
- package/lib/client.js +647 -409
- package/lib/http-utils.js +81 -0
- package/lib/index.js +342 -384
- package/lib/integrations.js +0 -1
- package/lib/prompt.js +42 -40
- package/lib/runner.js +48 -34
- package/lib/scheduler.js +33 -32
- package/lib/store.js +1 -1
- package/lib/telegram.js +27 -13
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -8,38 +8,296 @@ import { createKanbanCard } from './integrations.js';
|
|
|
8
8
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
import {
|
|
12
|
+
parseJsonBody,
|
|
13
|
+
isCrossOrigin,
|
|
14
|
+
pickPatchableFields,
|
|
15
|
+
SCRIPT_CONFIRM_HEADER,
|
|
16
|
+
} from './http-utils.js';
|
|
11
17
|
|
|
12
18
|
export const name = '@goodandready/dsh-cron';
|
|
13
19
|
export const inject = ['tools', 'webServer', 'settings', 'llm', 'agents', 'agentDefaultModel'];
|
|
14
20
|
|
|
15
21
|
export const Config = z.object({
|
|
16
|
-
botToken: z.string().role('secret').default('').description('Telegram Bot
|
|
17
|
-
chatId: z.string().default('').description('Telegram
|
|
18
|
-
notifyTelegram: z.boolean().default(false).description('
|
|
19
|
-
onlyOnFailure: z.boolean().default(false).description('
|
|
20
|
-
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('
|
|
22
|
+
botToken: z.string().role('secret').default('').description('Telegram Bot API token used to deliver task reports'),
|
|
23
|
+
chatId: z.string().default('').description('Telegram chat ID that receives task reports'),
|
|
24
|
+
notifyTelegram: z.boolean().default(false).description('Deliver task reports to Telegram for every task'),
|
|
25
|
+
onlyOnFailure: z.boolean().default(false).description('Deliver reports only for failed runs'),
|
|
26
|
+
kanbanBaseUrl: z.string().default('http://127.0.0.1:3000').description('Base URL of the dsh-kanban HTTP API'),
|
|
21
27
|
});
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const PATCHABLE_TASK_FIELDS = [
|
|
30
|
+
'title',
|
|
31
|
+
'schedule',
|
|
32
|
+
'scheduleText',
|
|
33
|
+
'prompt',
|
|
34
|
+
'type',
|
|
35
|
+
'delivery',
|
|
36
|
+
'provider',
|
|
37
|
+
'model',
|
|
38
|
+
'notifyTelegram',
|
|
39
|
+
'onlyOnFailure',
|
|
40
|
+
'timeoutSeconds',
|
|
41
|
+
'overlapPolicy',
|
|
42
|
+
'kanbanMode',
|
|
43
|
+
'status',
|
|
44
|
+
'oneShot',
|
|
45
|
+
];
|
|
37
46
|
|
|
38
47
|
function sendJson(res, statusCode, data) {
|
|
39
48
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
40
49
|
res.end(JSON.stringify(data));
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
function rejectCrossOrigin(req, res) {
|
|
53
|
+
if (isCrossOrigin(req)) {
|
|
54
|
+
sendJson(res, 403, { ok: false, error: 'Cross-origin request rejected' });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readBody(req, res) {
|
|
61
|
+
try {
|
|
62
|
+
return { body: await parseJsonBody(req) };
|
|
63
|
+
} catch (err) {
|
|
64
|
+
sendJson(res, err.statusCode || 400, { ok: false, error: err.message });
|
|
65
|
+
return { error: true };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Shared implementation for the /dsh-cron/tasks... and /dsh-cron/action...
|
|
71
|
+
* route families — the #66 alias is kept alive by the same code path instead
|
|
72
|
+
* of a duplicated handler.
|
|
73
|
+
*
|
|
74
|
+
* IMPORTANT: prefix routes must be registered WITHOUT a trailing slash —
|
|
75
|
+
* with one, the core degrades matching to exact-path equality and every
|
|
76
|
+
* deeper path unrouted (#96).
|
|
77
|
+
*/
|
|
78
|
+
function createCronApiHandler(store, scheduler, collection) {
|
|
79
|
+
return async function handleCronApi(req, res) {
|
|
80
|
+
try {
|
|
81
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
82
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
83
|
+
const scope = parts[1]; // 'tasks' | 'action'
|
|
84
|
+
const id = parts[2];
|
|
85
|
+
const action = parts[3];
|
|
86
|
+
|
|
87
|
+
// Collection level: /dsh-cron/tasks (list + create)
|
|
88
|
+
if (!id) {
|
|
89
|
+
if (scope === 'action' || !collection) {
|
|
90
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (req.method === 'GET') {
|
|
94
|
+
const status = url.searchParams.get('status') || 'all';
|
|
95
|
+
const query = url.searchParams.get('query') || '';
|
|
96
|
+
const list = store.list({ status, query });
|
|
97
|
+
const stats = store.getAggregatedStats();
|
|
98
|
+
sendJson(res, 200, { ok: true, tasks: list, recommendations: collection.recommendations, stats });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (req.method === 'POST') {
|
|
102
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
103
|
+
const { body, error } = await readBody(req, res);
|
|
104
|
+
if (error) return;
|
|
105
|
+
if (!body.title || !body.schedule || !body.prompt) {
|
|
106
|
+
sendJson(res, 400, { ok: false, error: 'Fields title, schedule and prompt are required' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const taskType = body.type === 'script' ? 'script' : 'llm';
|
|
110
|
+
if (taskType === 'script' && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
111
|
+
sendJson(res, 403, { ok: false, error: 'Creating script tasks over HTTP requires the x-dsh-cron-confirm: script header' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const parsed = parseScheduleExpression(body.schedule);
|
|
115
|
+
const current = body.id ? store.get(body.id) : null;
|
|
116
|
+
const resolvedStatus = body.status || (current ? current.status : 'active');
|
|
117
|
+
|
|
118
|
+
const task = store.set({
|
|
119
|
+
id: body.id,
|
|
120
|
+
title: body.title,
|
|
121
|
+
schedule: parsed.cronPattern || body.schedule,
|
|
122
|
+
scheduleText: body.scheduleText || parsed.humanText,
|
|
123
|
+
prompt: body.prompt,
|
|
124
|
+
type: taskType,
|
|
125
|
+
delivery: body.delivery || 'current',
|
|
126
|
+
status: resolvedStatus,
|
|
127
|
+
provider: body.provider || undefined,
|
|
128
|
+
model: body.model || undefined,
|
|
129
|
+
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : (current ? current.notifyTelegram : false),
|
|
130
|
+
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : (current ? current.onlyOnFailure : false),
|
|
131
|
+
timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : (current ? current.timeoutSeconds : 1800),
|
|
132
|
+
overlapPolicy: body.overlapPolicy || (current ? current.overlapPolicy : 'skip'),
|
|
133
|
+
kanbanMode: body.kanbanMode || (current ? current.kanbanMode : 'none'),
|
|
134
|
+
oneShot: Boolean(parsed.isOneShot || body.oneShot),
|
|
135
|
+
});
|
|
136
|
+
if (task.status === 'active') {
|
|
137
|
+
scheduler.scheduleTask(task);
|
|
138
|
+
} else {
|
|
139
|
+
scheduler.pauseTask(task.id);
|
|
140
|
+
}
|
|
141
|
+
sendJson(res, 200, { ok: true, task });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Item level: /dsh-cron/{tasks|action}/:id[/:action]
|
|
149
|
+
if (req.method !== 'GET' && rejectCrossOrigin(req, res)) return;
|
|
150
|
+
|
|
151
|
+
if (req.method === 'GET' && (action === 'history' || !action)) {
|
|
152
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
153
|
+
sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (req.method === 'POST' && action === 'run') {
|
|
158
|
+
await scheduler.triggerManualRun(id);
|
|
159
|
+
sendJson(res, 200, { ok: true, task: store.get(id) });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (req.method === 'POST' && action === 'pause') {
|
|
164
|
+
const task = scheduler.pauseTask(id);
|
|
165
|
+
if (!task) {
|
|
166
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
sendJson(res, 200, { ok: true, task });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (req.method === 'POST' && action === 'resume') {
|
|
174
|
+
const task = scheduler.resumeTask(id);
|
|
175
|
+
if (!task) {
|
|
176
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
sendJson(res, 200, { ok: true, task });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (req.method === 'POST' && (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle'))) {
|
|
184
|
+
const current = store.get(id);
|
|
185
|
+
if (!current) {
|
|
186
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const task = scheduler.toggleTask(id);
|
|
190
|
+
sendJson(res, 200, { ok: true, task });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (req.method === 'PATCH') {
|
|
195
|
+
const current = store.get(id);
|
|
196
|
+
if (!current) {
|
|
197
|
+
sendJson(res, 404, { ok: false, error: 'Task not found' });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const { body, error } = await readBody(req, res);
|
|
201
|
+
if (error) return;
|
|
202
|
+
// Whitelist patchable fields so stats/timestamps stay server-owned (#90)
|
|
203
|
+
const patchData = pickPatchableFields(body, PATCHABLE_TASK_FIELDS);
|
|
204
|
+
if (patchData.type === 'script' && current.type !== 'script' && req.headers[SCRIPT_CONFIRM_HEADER] !== 'script') {
|
|
205
|
+
sendJson(res, 403, { ok: false, error: 'Switching a task to the script type over HTTP requires the x-dsh-cron-confirm: script header' });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (patchData.schedule) {
|
|
209
|
+
const parsed = parseScheduleExpression(patchData.schedule);
|
|
210
|
+
patchData.schedule = parsed.cronPattern || patchData.schedule;
|
|
211
|
+
patchData.scheduleText = body.scheduleText || parsed.humanText;
|
|
212
|
+
patchData.oneShot = Boolean(parsed.isOneShot || body.oneShot);
|
|
213
|
+
}
|
|
214
|
+
const task = store.set({ ...current, ...patchData, id });
|
|
215
|
+
if (task.status === 'active') {
|
|
216
|
+
scheduler.scheduleTask(task);
|
|
217
|
+
} else {
|
|
218
|
+
scheduler.pauseTask(id);
|
|
219
|
+
}
|
|
220
|
+
sendJson(res, 200, { ok: true, task });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (req.method === 'DELETE' || (req.method === 'POST' && action === 'delete')) {
|
|
225
|
+
const current = store.get(id);
|
|
226
|
+
if (current) {
|
|
227
|
+
scheduler.pauseTask(id);
|
|
228
|
+
store.delete(id);
|
|
229
|
+
}
|
|
230
|
+
sendJson(res, 200, { ok: true });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Single implementation behind the cron_create_task / cron_schedule_task
|
|
243
|
+
* alias pair (#70, #92).
|
|
244
|
+
*/
|
|
245
|
+
function executeCreateTask(store, scheduler, args) {
|
|
246
|
+
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
247
|
+
const parsed = parseScheduleExpression(args.schedule);
|
|
248
|
+
const task = store.set({
|
|
249
|
+
title: args.title,
|
|
250
|
+
schedule: parsed.cronPattern || args.schedule,
|
|
251
|
+
scheduleText: parsed.humanText,
|
|
252
|
+
prompt: args.prompt,
|
|
253
|
+
type: taskType,
|
|
254
|
+
delivery: args.delivery || 'isolated',
|
|
255
|
+
status: 'active',
|
|
256
|
+
provider: args.provider,
|
|
257
|
+
model: args.model,
|
|
258
|
+
notifyTelegram: Boolean(args.notifyTelegram),
|
|
259
|
+
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
260
|
+
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
261
|
+
overlapPolicy: args.overlapPolicy || 'skip',
|
|
262
|
+
kanbanMode: args.kanbanMode || 'none',
|
|
263
|
+
oneShot: Boolean(parsed.isOneShot),
|
|
264
|
+
});
|
|
265
|
+
scheduler.scheduleTask(task);
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
message: `Task "${task.title}" scheduled successfully (${task.scheduleText})`,
|
|
269
|
+
task
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const createTaskParameters = {
|
|
274
|
+
title: { type: 'string', description: 'Short task name', required: true },
|
|
275
|
+
schedule: { type: 'string', description: 'Schedule: cron ("0 9 * * *"), interval ("every 2h") or one-shot ("in 30m", "at: 2026-09-05T15:00:00Z")', required: true },
|
|
276
|
+
prompt: { type: 'string', description: 'Prompt/instruction for the agent at run time', required: true },
|
|
277
|
+
type: { type: 'string', enum: ['prompt', 'script'], description: 'Task type' },
|
|
278
|
+
delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Run mode: current (in the current chat) or isolated (separate session)' },
|
|
279
|
+
provider: { type: 'string', description: 'Model provider (optional)' },
|
|
280
|
+
model: { type: 'string', description: 'Model (optional)' },
|
|
281
|
+
notifyTelegram: { type: 'boolean', description: 'Send the run report to Telegram' },
|
|
282
|
+
onlyOnFailure: { type: 'boolean', description: 'Send reports only for failures' },
|
|
283
|
+
timeoutSeconds: { type: 'number', description: 'Execution time limit in seconds' },
|
|
284
|
+
overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Overlap policy' },
|
|
285
|
+
kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Create a dsh-kanban card for runs' },
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const createTaskOutput = {
|
|
289
|
+
schema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
additionalProperties: true,
|
|
292
|
+
properties: {
|
|
293
|
+
success: { type: 'boolean' },
|
|
294
|
+
message: { type: 'string' },
|
|
295
|
+
task: { type: 'object', additionalProperties: true }
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
299
|
+
};
|
|
300
|
+
|
|
43
301
|
export function apply(ctx, config) {
|
|
44
302
|
const store = new TaskStore();
|
|
45
303
|
const runner = new SessionRunner(ctx);
|
|
@@ -74,94 +332,47 @@ export function apply(ctx, config) {
|
|
|
74
332
|
const recommendations = [
|
|
75
333
|
{
|
|
76
334
|
id: 'rec_daily_digest',
|
|
77
|
-
title: '
|
|
335
|
+
title: 'Daily digest',
|
|
78
336
|
schedule: '0 8 * * 1-5',
|
|
79
|
-
scheduleText: '
|
|
80
|
-
prompt: '
|
|
81
|
-
description: '
|
|
337
|
+
scheduleText: 'Weekdays at 08:00',
|
|
338
|
+
prompt: 'Prepare a brief morning digest: review the list of active tasks, open Gitea tickets and outline the key priorities for today.',
|
|
339
|
+
description: 'Start every weekday with a summary of calendar, unread mail and priorities',
|
|
82
340
|
icon: 'bell'
|
|
83
341
|
},
|
|
84
342
|
{
|
|
85
343
|
id: 'rec_weekly_review',
|
|
86
|
-
title: '
|
|
344
|
+
title: 'Weekly review',
|
|
87
345
|
schedule: '0 16 * * 5',
|
|
88
|
-
scheduleText: '
|
|
89
|
-
prompt: '
|
|
90
|
-
description: '
|
|
346
|
+
scheduleText: 'Fridays at 16:00',
|
|
347
|
+
prompt: 'Prepare a weekly report on the work done: the list of closed tasks, created PRs and the current status of ongoing projects.',
|
|
348
|
+
description: 'Every Friday create a short report on the completed work',
|
|
91
349
|
icon: 'clipboard'
|
|
92
350
|
},
|
|
93
351
|
{
|
|
94
352
|
id: 'rec_monitor_status',
|
|
95
|
-
title: '
|
|
353
|
+
title: 'Follow-up monitor',
|
|
96
354
|
schedule: '0 9 * * 1-5',
|
|
97
|
-
scheduleText: '
|
|
98
|
-
prompt: '
|
|
99
|
-
description: '
|
|
355
|
+
scheduleText: 'Weekdays at 09:00',
|
|
356
|
+
prompt: 'Review recent project activity, build statuses and flag everything that requires developer attention.',
|
|
357
|
+
description: 'Review recent project activity and flag anything that needs your attention',
|
|
100
358
|
icon: 'activity'
|
|
101
359
|
}
|
|
102
360
|
];
|
|
103
361
|
|
|
104
|
-
// 1. REST API
|
|
105
|
-
//
|
|
362
|
+
// 1. REST API — collection (list/create) + item actions in one prefix
|
|
363
|
+
// handler per route family; registered WITHOUT the trailing slash (#96)
|
|
106
364
|
ctx.effect(() => ctx.webServer.register({
|
|
107
|
-
kind: '
|
|
365
|
+
kind: 'prefix',
|
|
108
366
|
path: '/dsh-cron/tasks',
|
|
109
|
-
handler:
|
|
110
|
-
|
|
111
|
-
if (req.method === 'GET') {
|
|
112
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
113
|
-
const status = url.searchParams.get('status') || 'all';
|
|
114
|
-
const query = url.searchParams.get('query') || '';
|
|
115
|
-
const list = store.list({ status, query });
|
|
116
|
-
const stats = store.getAggregatedStats();
|
|
117
|
-
sendJson(res, 200, { ok: true, tasks: list, recommendations, stats });
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (req.method === 'POST') {
|
|
122
|
-
const body = await parseJsonBody(req);
|
|
123
|
-
if (!body.title || !body.schedule || !body.prompt) {
|
|
124
|
-
sendJson(res, 400, { ok: false, error: 'Поля title, schedule и prompt обязательны' });
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const parsed = parseScheduleExpression(body.schedule);
|
|
128
|
-
const current = body.id ? store.get(body.id) : null;
|
|
129
|
-
const resolvedStatus = body.status || (current ? current.status : 'active');
|
|
130
|
-
const taskType = body.type === 'script' ? 'script' : 'llm';
|
|
131
|
-
|
|
132
|
-
const task = store.set({
|
|
133
|
-
id: body.id,
|
|
134
|
-
title: body.title,
|
|
135
|
-
schedule: parsed.cronPattern || body.schedule,
|
|
136
|
-
scheduleText: body.scheduleText || parsed.humanText,
|
|
137
|
-
prompt: body.prompt,
|
|
138
|
-
type: taskType,
|
|
139
|
-
delivery: body.delivery || 'current',
|
|
140
|
-
status: resolvedStatus,
|
|
141
|
-
provider: body.provider || undefined,
|
|
142
|
-
model: body.model || undefined,
|
|
143
|
-
notifyTelegram: body.notifyTelegram !== undefined ? Boolean(body.notifyTelegram) : (current ? current.notifyTelegram : false),
|
|
144
|
-
onlyOnFailure: body.onlyOnFailure !== undefined ? Boolean(body.onlyOnFailure) : (current ? current.onlyOnFailure : false),
|
|
145
|
-
timeoutSeconds: body.timeoutSeconds !== undefined ? Number(body.timeoutSeconds) : (current ? current.timeoutSeconds : 1800),
|
|
146
|
-
overlapPolicy: body.overlapPolicy || (current ? current.overlapPolicy : 'skip'),
|
|
147
|
-
kanbanMode: body.kanbanMode || (current ? current.kanbanMode : 'none'),
|
|
148
|
-
oneShot: Boolean(parsed.isOneShot || body.oneShot),
|
|
149
|
-
});
|
|
150
|
-
if (task.status === 'active') {
|
|
151
|
-
scheduler.scheduleTask(task);
|
|
152
|
-
} else {
|
|
153
|
-
scheduler.pauseTask(task.id);
|
|
154
|
-
}
|
|
155
|
-
sendJson(res, 200, { ok: true, task });
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
367
|
+
handler: createCronApiHandler(store, scheduler, { recommendations })
|
|
368
|
+
}), 'dsh-cron: /tasks[...]');
|
|
158
369
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}), 'dsh-cron: /
|
|
370
|
+
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
371
|
+
ctx.effect(() => ctx.webServer.register({
|
|
372
|
+
kind: 'prefix',
|
|
373
|
+
path: '/dsh-cron/action',
|
|
374
|
+
handler: createCronApiHandler(store, scheduler, null)
|
|
375
|
+
}), 'dsh-cron: /action/:id/:action');
|
|
165
376
|
|
|
166
377
|
// GET /dsh-cron/models
|
|
167
378
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -177,6 +388,7 @@ export function apply(ctx, config) {
|
|
|
177
388
|
kind: 'exact',
|
|
178
389
|
path: '/dsh-cron/chat/start',
|
|
179
390
|
handler: async (req, res) => {
|
|
391
|
+
if (req.method === 'POST' && rejectCrossOrigin(req, res)) return;
|
|
180
392
|
await chatStartHandler(ctx, req, res, parseJsonBody, sendJson, randomUUID);
|
|
181
393
|
}
|
|
182
394
|
}), 'dsh-cron: /chat/start');
|
|
@@ -192,14 +404,16 @@ export function apply(ctx, config) {
|
|
|
192
404
|
return;
|
|
193
405
|
}
|
|
194
406
|
if (req.method === 'POST') {
|
|
195
|
-
|
|
407
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
408
|
+
const { body, error } = await readBody(req, res);
|
|
409
|
+
if (error) return;
|
|
196
410
|
store.saveSettings(body);
|
|
197
411
|
sendJson(res, 200, { ok: true, settings: store.getClientSettings() });
|
|
198
412
|
return;
|
|
199
413
|
}
|
|
200
414
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
201
415
|
} catch (err) {
|
|
202
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
416
|
+
sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
|
|
203
417
|
}
|
|
204
418
|
}
|
|
205
419
|
}), 'dsh-cron: /settings');
|
|
@@ -214,19 +428,21 @@ export function apply(ctx, config) {
|
|
|
214
428
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
215
429
|
return;
|
|
216
430
|
}
|
|
217
|
-
|
|
431
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
432
|
+
const { body, error } = await readBody(req, res);
|
|
433
|
+
if (error) return;
|
|
218
434
|
const settings = store.getSettings();
|
|
219
435
|
const botToken = body.botToken || settings.botToken;
|
|
220
436
|
const chatId = body.chatId || settings.chatId;
|
|
221
437
|
|
|
222
438
|
if (!botToken || !chatId) {
|
|
223
|
-
sendJson(res, 400, { ok: false, error: '
|
|
439
|
+
sendJson(res, 400, { ok: false, error: 'botToken or chatId is not configured' });
|
|
224
440
|
return;
|
|
225
441
|
}
|
|
226
442
|
|
|
227
|
-
const text = `🔔
|
|
443
|
+
const text = `🔔 *dsh-cron test notification*\n\nConnection between DSH Cron and Telegram is working! Checked at: \`${new Date().toISOString()}\``;
|
|
228
444
|
await sendTelegramMessage({ botToken, chatId, text });
|
|
229
|
-
sendJson(res, 200, { ok: true, message: '
|
|
445
|
+
sendJson(res, 200, { ok: true, message: 'Test message sent successfully' });
|
|
230
446
|
} catch (err) {
|
|
231
447
|
sendJson(res, 500, { ok: false, error: err.message });
|
|
232
448
|
}
|
|
@@ -243,13 +459,15 @@ export function apply(ctx, config) {
|
|
|
243
459
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
244
460
|
return;
|
|
245
461
|
}
|
|
246
|
-
|
|
462
|
+
if (rejectCrossOrigin(req, res)) return;
|
|
463
|
+
const { body, error } = await readBody(req, res);
|
|
464
|
+
if (error) return;
|
|
247
465
|
const settings = store.getSettings();
|
|
248
466
|
const kanbanBaseUrl = body.kanbanBaseUrl || settings.kanbanBaseUrl || 'http://127.0.0.1:3000';
|
|
249
467
|
|
|
250
468
|
const result = await createKanbanCard({
|
|
251
|
-
title:
|
|
252
|
-
body:
|
|
469
|
+
title: '[Test] dsh-cron -> Kanban connectivity check',
|
|
470
|
+
body: `Test card created automatically by the \`dsh-cron\` module at ${new Date().toISOString()}.`,
|
|
253
471
|
board: 'main',
|
|
254
472
|
column: 'backlog',
|
|
255
473
|
labels: ['cron', 'test'],
|
|
@@ -267,289 +485,29 @@ export function apply(ctx, config) {
|
|
|
267
485
|
}
|
|
268
486
|
}), 'dsh-cron: /kanban/test');
|
|
269
487
|
|
|
270
|
-
// PATCH & DELETE /dsh-cron/tasks/:id
|
|
271
|
-
ctx.effect(() => ctx.webServer.register({
|
|
272
|
-
kind: 'prefix',
|
|
273
|
-
path: '/dsh-cron/tasks/',
|
|
274
|
-
handler: async (req, res) => {
|
|
275
|
-
try {
|
|
276
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
277
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
278
|
-
const id = parts[2];
|
|
279
|
-
const action = parts[3];
|
|
280
|
-
|
|
281
|
-
if (!id) {
|
|
282
|
-
sendJson(res, 400, { ok: false, error: 'ID задачи не указан' });
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (req.method === 'GET' && action === 'history') {
|
|
287
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
288
|
-
const history = store.getHistory(id, limit);
|
|
289
|
-
sendJson(res, 200, { ok: true, history });
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (req.method === 'POST' && action === 'run') {
|
|
294
|
-
await scheduler.triggerManualRun(id);
|
|
295
|
-
const task = store.get(id);
|
|
296
|
-
sendJson(res, 200, { ok: true, task });
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (req.method === 'POST' && action === 'pause') {
|
|
301
|
-
const task = scheduler.pauseTask(id);
|
|
302
|
-
if (!task) {
|
|
303
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
sendJson(res, 200, { ok: true, task });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (req.method === 'POST' && action === 'resume') {
|
|
311
|
-
const task = scheduler.resumeTask(id);
|
|
312
|
-
if (!task) {
|
|
313
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
sendJson(res, 200, { ok: true, task });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (req.method === 'POST' && (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle'))) {
|
|
321
|
-
const current = store.get(id);
|
|
322
|
-
if (!current) {
|
|
323
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const task = scheduler.toggleTask(id);
|
|
327
|
-
sendJson(res, 200, { ok: true, task });
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (req.method === 'PATCH') {
|
|
332
|
-
const body = await parseJsonBody(req);
|
|
333
|
-
const current = store.get(id);
|
|
334
|
-
if (!current) {
|
|
335
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
let patchData = { ...body };
|
|
339
|
-
if (body.schedule) {
|
|
340
|
-
const parsed = parseScheduleExpression(body.schedule);
|
|
341
|
-
patchData.schedule = parsed.cronPattern || body.schedule;
|
|
342
|
-
patchData.scheduleText = body.scheduleText || parsed.humanText;
|
|
343
|
-
patchData.oneShot = Boolean(parsed.isOneShot || body.oneShot);
|
|
344
|
-
}
|
|
345
|
-
const task = store.set({ ...current, ...patchData, id });
|
|
346
|
-
if (task.status === 'active') {
|
|
347
|
-
scheduler.scheduleTask(task);
|
|
348
|
-
} else {
|
|
349
|
-
scheduler.pauseTask(id);
|
|
350
|
-
}
|
|
351
|
-
sendJson(res, 200, { ok: true, task });
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (req.method === 'DELETE' || (req.method === 'POST' && action === 'delete')) {
|
|
356
|
-
const current = store.get(id);
|
|
357
|
-
if (current) {
|
|
358
|
-
scheduler.pauseTask(id);
|
|
359
|
-
store.delete(id);
|
|
360
|
-
}
|
|
361
|
-
sendJson(res, 200, { ok: true });
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
366
|
-
} catch (err) {
|
|
367
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}), 'dsh-cron: /tasks/:id');
|
|
371
|
-
|
|
372
|
-
// Alias route /dsh-cron/action/:id/:action for backward compatibility (#66)
|
|
373
|
-
ctx.effect(() => ctx.webServer.register({
|
|
374
|
-
kind: 'prefix',
|
|
375
|
-
path: '/dsh-cron/action/',
|
|
376
|
-
handler: async (req, res) => {
|
|
377
|
-
try {
|
|
378
|
-
const url = new URL(req.url, 'http://127.0.0.1');
|
|
379
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
380
|
-
const id = parts[2];
|
|
381
|
-
const action = parts[3];
|
|
382
|
-
|
|
383
|
-
if (!id) {
|
|
384
|
-
sendJson(res, 400, { ok: false, error: 'ID задачи не указан' });
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (action === 'history' || (req.method === 'GET' && !action)) {
|
|
389
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
390
|
-
const history = store.getHistory(id, limit);
|
|
391
|
-
sendJson(res, 200, { ok: true, history });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (action === 'run') {
|
|
396
|
-
await scheduler.triggerManualRun(id);
|
|
397
|
-
const task = store.get(id);
|
|
398
|
-
sendJson(res, 200, { ok: true, task });
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (action === 'toggle') {
|
|
403
|
-
const current = store.get(id);
|
|
404
|
-
if (!current) {
|
|
405
|
-
sendJson(res, 404, { ok: false, error: 'Задача не найдена' });
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
const task = scheduler.toggleTask(id);
|
|
409
|
-
sendJson(res, 200, { ok: true, task });
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (action === 'delete') {
|
|
414
|
-
const current = store.get(id);
|
|
415
|
-
if (current) {
|
|
416
|
-
scheduler.pauseTask(id);
|
|
417
|
-
store.delete(id);
|
|
418
|
-
}
|
|
419
|
-
sendJson(res, 200, { ok: true });
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
sendJson(res, 405, { ok: false, error: 'Action not supported' });
|
|
424
|
-
} catch (err) {
|
|
425
|
-
sendJson(res, 500, { ok: false, error: err.message });
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}), 'dsh-cron: /action/:id/:action');
|
|
429
|
-
|
|
430
488
|
// 2. Tools
|
|
431
489
|
ctx.tools.register(defineTool({
|
|
432
490
|
name: 'cron_create_task',
|
|
433
|
-
description: '
|
|
434
|
-
parameters:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
prompt: { type: 'string', description: 'Промпт/инструкция для агента при запуске', required: true },
|
|
438
|
-
type: { type: 'string', enum: ['prompt', 'script'], description: 'Тип задачи' },
|
|
439
|
-
delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Режим запуска: current (в текущем чате) или isolated (изолированная сессия)' },
|
|
440
|
-
provider: { type: 'string', description: 'Провайдер модели (опционально)' },
|
|
441
|
-
model: { type: 'string', description: 'Модель (опционально)' },
|
|
442
|
-
notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
|
|
443
|
-
onlyOnFailure: { type: 'boolean', description: 'Отправлять отчет только при сбоях' },
|
|
444
|
-
timeoutSeconds: { type: 'number', description: 'Лимит времени выполнения в секундах' },
|
|
445
|
-
overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Политика перекрытия' },
|
|
446
|
-
kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Создавать карточку в Kanban' },
|
|
447
|
-
},
|
|
448
|
-
output: {
|
|
449
|
-
schema: {
|
|
450
|
-
type: 'object',
|
|
451
|
-
additionalProperties: true,
|
|
452
|
-
properties: {
|
|
453
|
-
success: { type: 'boolean' },
|
|
454
|
-
message: { type: 'string' },
|
|
455
|
-
task: { type: 'object', additionalProperties: true }
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
459
|
-
},
|
|
460
|
-
execute: async (args) => {
|
|
461
|
-
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
462
|
-
const parsed = parseScheduleExpression(args.schedule);
|
|
463
|
-
const task = store.set({
|
|
464
|
-
title: args.title,
|
|
465
|
-
schedule: parsed.cronPattern || args.schedule,
|
|
466
|
-
scheduleText: parsed.humanText,
|
|
467
|
-
prompt: args.prompt,
|
|
468
|
-
type: taskType,
|
|
469
|
-
delivery: args.delivery || 'isolated',
|
|
470
|
-
status: 'active',
|
|
471
|
-
provider: args.provider,
|
|
472
|
-
model: args.model,
|
|
473
|
-
notifyTelegram: Boolean(args.notifyTelegram),
|
|
474
|
-
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
475
|
-
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
476
|
-
overlapPolicy: args.overlapPolicy || 'skip',
|
|
477
|
-
kanbanMode: args.kanbanMode || 'none',
|
|
478
|
-
oneShot: Boolean(parsed.isOneShot),
|
|
479
|
-
});
|
|
480
|
-
scheduler.scheduleTask(task);
|
|
481
|
-
return {
|
|
482
|
-
success: true,
|
|
483
|
-
message: `Задача "${task.title}" успешно запланирована (${task.scheduleText})`,
|
|
484
|
-
task
|
|
485
|
-
};
|
|
486
|
-
}
|
|
491
|
+
description: 'Create a new scheduled cron task (cron schedule, or one-shot via "at: <ISO timestamp>" or "in 20m")',
|
|
492
|
+
parameters: createTaskParameters,
|
|
493
|
+
output: createTaskOutput,
|
|
494
|
+
execute: async (args) => executeCreateTask(store, scheduler, args)
|
|
487
495
|
}));
|
|
488
496
|
|
|
489
497
|
// Alias tool cron_schedule_task for compatibility with agent prompts (#70)
|
|
490
498
|
ctx.tools.register(defineTool({
|
|
491
499
|
name: 'cron_schedule_task',
|
|
492
|
-
description: '
|
|
493
|
-
parameters:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
prompt: { type: 'string', description: 'Промпт/инструкция для агента при запуске', required: true },
|
|
497
|
-
type: { type: 'string', enum: ['llm', 'prompt', 'script'], description: 'Тип задачи' },
|
|
498
|
-
delivery: { type: 'string', enum: ['current', 'isolated'], description: 'Режим запуска' },
|
|
499
|
-
provider: { type: 'string', description: 'Провайдер модели (опционально)' },
|
|
500
|
-
model: { type: 'string', description: 'Модель (опционально)' },
|
|
501
|
-
notifyTelegram: { type: 'boolean', description: 'Отправлять отчет в Telegram' },
|
|
502
|
-
onlyOnFailure: { type: 'boolean', description: 'Отправлять отчет только при сбоях' },
|
|
503
|
-
timeoutSeconds: { type: 'number', description: 'Лимит времени выполнения в секундах' },
|
|
504
|
-
overlapPolicy: { type: 'string', enum: ['skip', 'queue', 'replace'], description: 'Политика перекрытия' },
|
|
505
|
-
kanbanMode: { type: 'string', enum: ['none', 'on_failure', 'always'], description: 'Создавать карточку в Kanban' },
|
|
506
|
-
},
|
|
507
|
-
output: {
|
|
508
|
-
schema: {
|
|
509
|
-
type: 'object',
|
|
510
|
-
additionalProperties: true,
|
|
511
|
-
properties: {
|
|
512
|
-
success: { type: 'boolean' },
|
|
513
|
-
message: { type: 'string' },
|
|
514
|
-
task: { type: 'object', additionalProperties: true }
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
render: (_args, val) => [{ type: 'text', text: val.message || JSON.stringify(val) }]
|
|
518
|
-
},
|
|
519
|
-
execute: async (args) => {
|
|
520
|
-
const taskType = args.type === 'script' ? 'script' : 'llm';
|
|
521
|
-
const parsed = parseScheduleExpression(args.schedule);
|
|
522
|
-
const task = store.set({
|
|
523
|
-
title: args.title,
|
|
524
|
-
schedule: parsed.cronPattern || args.schedule,
|
|
525
|
-
scheduleText: parsed.humanText,
|
|
526
|
-
prompt: args.prompt,
|
|
527
|
-
type: taskType,
|
|
528
|
-
delivery: args.delivery || 'isolated',
|
|
529
|
-
status: 'active',
|
|
530
|
-
provider: args.provider,
|
|
531
|
-
model: args.model,
|
|
532
|
-
notifyTelegram: Boolean(args.notifyTelegram),
|
|
533
|
-
onlyOnFailure: Boolean(args.onlyOnFailure),
|
|
534
|
-
timeoutSeconds: Number(args.timeoutSeconds) || 1800,
|
|
535
|
-
overlapPolicy: args.overlapPolicy || 'skip',
|
|
536
|
-
kanbanMode: args.kanbanMode || 'none',
|
|
537
|
-
oneShot: Boolean(parsed.isOneShot),
|
|
538
|
-
});
|
|
539
|
-
scheduler.scheduleTask(task);
|
|
540
|
-
return {
|
|
541
|
-
success: true,
|
|
542
|
-
message: `Задача "${task.title}" успешно запланирована (${task.scheduleText})`,
|
|
543
|
-
task
|
|
544
|
-
};
|
|
545
|
-
}
|
|
500
|
+
description: 'Alias for cron_create_task: create a new scheduled cron task',
|
|
501
|
+
parameters: createTaskParameters,
|
|
502
|
+
output: createTaskOutput,
|
|
503
|
+
execute: async (args) => executeCreateTask(store, scheduler, args)
|
|
546
504
|
}));
|
|
547
505
|
|
|
548
506
|
ctx.tools.register(defineTool({
|
|
549
507
|
name: 'cron_list_tasks',
|
|
550
|
-
description: '
|
|
508
|
+
description: 'List all scheduled cron tasks',
|
|
551
509
|
parameters: {
|
|
552
|
-
status: { type: 'string', enum: ['all', 'active', 'paused', 'completed'], description: '
|
|
510
|
+
status: { type: 'string', enum: ['all', 'active', 'paused', 'completed'], description: 'Filter by status' }
|
|
553
511
|
},
|
|
554
512
|
output: {
|
|
555
513
|
schema: {
|
|
@@ -560,7 +518,7 @@ export function apply(ctx, config) {
|
|
|
560
518
|
tasks: { type: 'array', items: { type: 'object', additionalProperties: true } }
|
|
561
519
|
}
|
|
562
520
|
},
|
|
563
|
-
render: (_args, val) => [{ type: 'text', text:
|
|
521
|
+
render: (_args, val) => [{ type: 'text', text: `Total tasks: ${val.count}` }]
|
|
564
522
|
},
|
|
565
523
|
execute: async (args) => {
|
|
566
524
|
const tasks = store.list({ status: args.status || 'all' });
|
|
@@ -583,9 +541,9 @@ export function apply(ctx, config) {
|
|
|
583
541
|
|
|
584
542
|
ctx.tools.register(defineTool({
|
|
585
543
|
name: 'cron_pause_task',
|
|
586
|
-
description: '
|
|
544
|
+
description: 'Pause a task by its ID',
|
|
587
545
|
parameters: {
|
|
588
|
-
id: { type: 'string', description: '
|
|
546
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
589
547
|
},
|
|
590
548
|
output: {
|
|
591
549
|
schema: {
|
|
@@ -600,16 +558,16 @@ export function apply(ctx, config) {
|
|
|
600
558
|
},
|
|
601
559
|
execute: async (args) => {
|
|
602
560
|
const task = scheduler.pauseTask(args.id);
|
|
603
|
-
if (!task) return { success: false, message: '
|
|
604
|
-
return { success: true, message:
|
|
561
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
562
|
+
return { success: true, message: `Task "${task.title}" paused` };
|
|
605
563
|
}
|
|
606
564
|
}));
|
|
607
565
|
|
|
608
566
|
ctx.tools.register(defineTool({
|
|
609
567
|
name: 'cron_resume_task',
|
|
610
|
-
description: '
|
|
568
|
+
description: 'Resume a paused task',
|
|
611
569
|
parameters: {
|
|
612
|
-
id: { type: 'string', description: '
|
|
570
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
613
571
|
},
|
|
614
572
|
output: {
|
|
615
573
|
schema: {
|
|
@@ -624,16 +582,16 @@ export function apply(ctx, config) {
|
|
|
624
582
|
},
|
|
625
583
|
execute: async (args) => {
|
|
626
584
|
const task = scheduler.resumeTask(args.id);
|
|
627
|
-
if (!task) return { success: false, message: '
|
|
628
|
-
return { success: true, message:
|
|
585
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
586
|
+
return { success: true, message: `Task "${task.title}" resumed (${task.scheduleText})` };
|
|
629
587
|
}
|
|
630
588
|
}));
|
|
631
589
|
|
|
632
590
|
ctx.tools.register(defineTool({
|
|
633
591
|
name: 'cron_delete_task',
|
|
634
|
-
description: '
|
|
592
|
+
description: 'Delete a task by its ID',
|
|
635
593
|
parameters: {
|
|
636
|
-
id: { type: 'string', description: '
|
|
594
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
637
595
|
},
|
|
638
596
|
output: {
|
|
639
597
|
schema: {
|
|
@@ -649,15 +607,15 @@ export function apply(ctx, config) {
|
|
|
649
607
|
execute: async (args) => {
|
|
650
608
|
scheduler.pauseTask(args.id);
|
|
651
609
|
const ok = store.delete(args.id);
|
|
652
|
-
return { success: ok, message: ok ? '
|
|
610
|
+
return { success: ok, message: ok ? 'Task deleted' : 'Task not found' };
|
|
653
611
|
}
|
|
654
612
|
}));
|
|
655
613
|
|
|
656
614
|
ctx.tools.register(defineTool({
|
|
657
615
|
name: 'cron_run_task',
|
|
658
|
-
description: '
|
|
616
|
+
description: 'Trigger a task immediately, out of band',
|
|
659
617
|
parameters: {
|
|
660
|
-
id: { type: 'string', description: '
|
|
618
|
+
id: { type: 'string', description: 'Task identifier', required: true }
|
|
661
619
|
},
|
|
662
620
|
output: {
|
|
663
621
|
schema: {
|
|
@@ -672,9 +630,9 @@ export function apply(ctx, config) {
|
|
|
672
630
|
},
|
|
673
631
|
execute: async (args) => {
|
|
674
632
|
const task = store.get(args.id);
|
|
675
|
-
if (!task) return { success: false, message: '
|
|
633
|
+
if (!task) return { success: false, message: 'Task not found' };
|
|
676
634
|
scheduler.triggerManualRun(args.id);
|
|
677
|
-
return { success: true, message:
|
|
635
|
+
return { success: true, message: `Manual run started for task "${task.title}"` };
|
|
678
636
|
}
|
|
679
637
|
}));
|
|
680
638
|
}
|