clawflowbang 1.0.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.
@@ -0,0 +1,512 @@
1
+ /**
2
+ * CronManager - จัดการ cronjobs
3
+ */
4
+
5
+ const fs = require('fs-extra');
6
+ const path = require('path');
7
+ const cron = require('node-cron');
8
+ const chalk = require('chalk');
9
+ const OpenClawCLI = require('./OpenClawCLI');
10
+ const { normalizeCronExpression } = require('./CronFormat');
11
+
12
+ class CronManager {
13
+ constructor(configManager) {
14
+ this.configManager = configManager;
15
+ this.tasks = new Map();
16
+ this.jobsFile = configManager.getCronJobsFilePath();
17
+ this.openclawCLI = new OpenClawCLI(configManager);
18
+ this.useOpenClawCron = this.openclawCLI.hasOpenClaw();
19
+
20
+ if (!this.useOpenClawCron) {
21
+ this.ensureJobsFile();
22
+ }
23
+ }
24
+
25
+ /**
26
+ * สร้างไฟล์ jobs ถ้ายังไม่มี
27
+ */
28
+ ensureJobsFile() {
29
+ fs.ensureDirSync(path.dirname(this.jobsFile));
30
+ if (!fs.existsSync(this.jobsFile)) {
31
+ fs.writeJsonSync(this.jobsFile, { jobs: [] }, { spaces: 2 });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * อ่านรายการ jobs
37
+ */
38
+ getJobs() {
39
+ if (!fs.existsSync(this.jobsFile)) {
40
+ return [];
41
+ }
42
+
43
+ const data = fs.readJsonSync(this.jobsFile);
44
+ return data.jobs || [];
45
+ }
46
+
47
+ /**
48
+ * บันทึกรายการ jobs
49
+ */
50
+ saveJobs(jobs) {
51
+ fs.writeJsonSync(this.jobsFile, { jobs }, { spaces: 2 });
52
+ }
53
+
54
+ /**
55
+ * เพิ่ม cronjob
56
+ */
57
+ async add(skillName, schedule, params = {}, description = '') {
58
+ const normalizedSchedule = normalizeCronExpression(schedule);
59
+
60
+ if (this.useOpenClawCron) {
61
+ const uniqueName = `cfh:${skillName}:${Date.now()}`;
62
+ const message = `Run skill "${skillName}" with params: ${JSON.stringify(params)}`;
63
+ const created = await this.openclawCLI.addCronJob({
64
+ name: uniqueName,
65
+ description: description || `Run ${skillName}`,
66
+ schedule: normalizedSchedule,
67
+ message,
68
+ });
69
+
70
+ let jobId = created.jobId;
71
+ if (!jobId) {
72
+ const jobs = await this.openclawCLI.listCronJobs();
73
+ const matched = jobs.find((job) => job.name === uniqueName);
74
+ jobId = matched?.id || null;
75
+ }
76
+
77
+ if (!jobId) {
78
+ throw new Error('สร้าง cronjob ผ่าน openclaw สำเร็จ แต่ไม่สามารถระบุ job id ได้');
79
+ }
80
+
81
+ this.configManager.addCron({
82
+ id: jobId,
83
+ skill: skillName,
84
+ schedule: normalizedSchedule,
85
+ description: description || `Run ${skillName}`,
86
+ });
87
+
88
+ console.log(chalk.green(`✓ เพิ่ม cronjob (openclaw): ${skillName} (${normalizedSchedule})`));
89
+
90
+ return {
91
+ id: jobId,
92
+ skill: skillName,
93
+ schedule: normalizedSchedule,
94
+ params,
95
+ description: description || `Run ${skillName}`,
96
+ enabled: true,
97
+ };
98
+ }
99
+
100
+ const jobs = this.getJobs();
101
+ const jobId = `cron_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
102
+
103
+ const job = {
104
+ id: jobId,
105
+ skill: skillName,
106
+ schedule: normalizedSchedule,
107
+ params,
108
+ description: description || `Run ${skillName}`,
109
+ enabled: true,
110
+ createdAt: new Date().toISOString(),
111
+ lastRun: null,
112
+ nextRun: this.getNextRun(normalizedSchedule),
113
+ runCount: 0,
114
+ errorCount: 0,
115
+ };
116
+
117
+ jobs.push(job);
118
+ this.saveJobs(jobs);
119
+
120
+ // บันทึกลง config ด้วย
121
+ this.configManager.addCron({
122
+ id: jobId,
123
+ skill: skillName,
124
+ schedule: normalizedSchedule,
125
+ description,
126
+ });
127
+
128
+ // สร้าง cron script
129
+ this.createCronScript(job);
130
+
131
+ // เริ่มต้น task (ถ้าเปิดใช้งาน)
132
+ if (job.enabled) {
133
+ this.startJob(job);
134
+ }
135
+
136
+ console.log(chalk.green(`✓ เพิ่ม cronjob: ${skillName} (${normalizedSchedule})`));
137
+
138
+ return job;
139
+ }
140
+
141
+ /**
142
+ * แก้ไข cronjob
143
+ */
144
+ async edit(jobId, updates = {}) {
145
+ const normalized = { ...updates };
146
+
147
+ if (updates.schedule) {
148
+ normalized.schedule = normalizeCronExpression(updates.schedule);
149
+ }
150
+
151
+ if (this.useOpenClawCron) {
152
+ const tracked = this.configManager.getCrons().find((c) => c.id === jobId);
153
+ const message =
154
+ Object.prototype.hasOwnProperty.call(updates, 'params') && tracked?.skill
155
+ ? `Run skill "${tracked.skill}" with params: ${JSON.stringify(updates.params || {})}`
156
+ : undefined;
157
+
158
+ if (Object.prototype.hasOwnProperty.call(updates, 'params') && !tracked?.skill) {
159
+ throw new Error('แก้ไข params ไม่ได้ เพราะไม่พบ mapping skill ของ cron นี้ใน config');
160
+ }
161
+
162
+ await this.openclawCLI.editCronJob(jobId, {
163
+ schedule: normalized.schedule,
164
+ description: normalized.description,
165
+ message,
166
+ });
167
+
168
+ this.configManager.updateCron(jobId, {
169
+ ...(normalized.schedule ? { schedule: normalized.schedule } : {}),
170
+ ...(typeof normalized.description === 'string' ? { description: normalized.description } : {}),
171
+ ...(Object.prototype.hasOwnProperty.call(updates, 'params') ? { params: updates.params } : {}),
172
+ });
173
+
174
+ return { id: jobId, ...normalized };
175
+ }
176
+
177
+ const jobs = this.getJobs();
178
+ const job = jobs.find((j) => j.id === jobId);
179
+
180
+ if (!job) {
181
+ throw new Error(`ไม่พบ cronjob ID: ${jobId}`);
182
+ }
183
+
184
+ if (normalized.schedule) {
185
+ job.schedule = normalized.schedule;
186
+ job.nextRun = this.getNextRun(normalized.schedule);
187
+ }
188
+
189
+ if (typeof normalized.description === 'string') {
190
+ job.description = normalized.description;
191
+ }
192
+
193
+ if (Object.prototype.hasOwnProperty.call(updates, 'params')) {
194
+ job.params = updates.params || {};
195
+ }
196
+
197
+ this.saveJobs(jobs);
198
+ this.configManager.updateCron(jobId, {
199
+ ...(normalized.schedule ? { schedule: normalized.schedule } : {}),
200
+ ...(typeof normalized.description === 'string' ? { description: normalized.description } : {}),
201
+ ...(Object.prototype.hasOwnProperty.call(updates, 'params') ? { params: updates.params } : {}),
202
+ });
203
+
204
+ return job;
205
+ }
206
+
207
+ /**
208
+ * ลบ cronjob
209
+ */
210
+ async remove(jobId) {
211
+ if (this.useOpenClawCron) {
212
+ await this.openclawCLI.removeCronJob(jobId);
213
+ this.configManager.removeCron(jobId);
214
+ console.log(chalk.green(`✓ ลบ cronjob (openclaw): ${jobId}`));
215
+ return { success: true, removed: { id: jobId } };
216
+ }
217
+
218
+ let jobs = this.getJobs();
219
+ const job = jobs.find(j => j.id === jobId);
220
+
221
+ if (!job) {
222
+ throw new Error(`ไม่พบ cronjob ID: ${jobId}`);
223
+ }
224
+
225
+ // หยุด task
226
+ this.stopJob(jobId);
227
+
228
+ // ลบไฟล์ script
229
+ this.removeCronScript(jobId);
230
+
231
+ // ลบจากรายการ
232
+ jobs = jobs.filter(j => j.id !== jobId);
233
+ this.saveJobs(jobs);
234
+
235
+ // ลบจาก config
236
+ this.configManager.removeCron(jobId);
237
+
238
+ console.log(chalk.green(`✓ ลบ cronjob: ${job.skill}`));
239
+
240
+ return { success: true, removed: job };
241
+ }
242
+
243
+ /**
244
+ * แสดงรายการ cronjobs
245
+ */
246
+ list() {
247
+ if (this.useOpenClawCron) {
248
+ return this.listViaOpenClaw();
249
+ }
250
+
251
+ const jobs = this.getJobs();
252
+
253
+ if (jobs.length === 0) {
254
+ console.log(chalk.gray('ไม่มี cronjob ที่ตั้งไว้'));
255
+ return [];
256
+ }
257
+
258
+ return jobs.map(job => ({
259
+ id: job.id,
260
+ skill: job.skill,
261
+ schedule: job.schedule,
262
+ description: job.description,
263
+ enabled: job.enabled,
264
+ lastRun: job.lastRun,
265
+ nextRun: job.nextRun,
266
+ runCount: job.runCount,
267
+ }));
268
+ }
269
+
270
+ /**
271
+ * แสดงรายการ cronjobs ผ่าน OpenClaw CLI
272
+ */
273
+ async listViaOpenClaw() {
274
+ const jobs = await this.openclawCLI.listCronJobs();
275
+
276
+ if (!jobs || jobs.length === 0) {
277
+ console.log(chalk.gray('ไม่มี cronjob ที่ตั้งไว้'));
278
+ return [];
279
+ }
280
+
281
+ return jobs.map((job) => ({
282
+ id: job.id,
283
+ skill: job.name || 'unknown',
284
+ schedule: job.schedule?.expr || job.schedule?.kind || '-',
285
+ description: job.description || '',
286
+ enabled: job.enabled !== false,
287
+ lastRun: job.lastRunAt || null,
288
+ nextRun: job.nextRunAt || null,
289
+ runCount: job.runCount || 0,
290
+ }));
291
+ }
292
+
293
+ /**
294
+ * เริ่มต้น job
295
+ */
296
+ startJob(job) {
297
+ if (this.tasks.has(job.id)) {
298
+ this.stopJob(job.id);
299
+ }
300
+
301
+ const task = cron.schedule(job.schedule, async () => {
302
+ await this.executeJob(job.id);
303
+ }, {
304
+ scheduled: true,
305
+ timezone: process.env.TZ || 'Asia/Bangkok',
306
+ });
307
+
308
+ this.tasks.set(job.id, task);
309
+ }
310
+
311
+ /**
312
+ * หยุด job
313
+ */
314
+ stopJob(jobId) {
315
+ const task = this.tasks.get(jobId);
316
+ if (task) {
317
+ task.stop();
318
+ if (typeof task.destroy === 'function') {
319
+ task.destroy();
320
+ }
321
+ this.tasks.delete(jobId);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * รัน job
327
+ */
328
+ async executeJob(jobId) {
329
+ const jobs = this.getJobs();
330
+ const job = jobs.find(j => j.id === jobId);
331
+
332
+ if (!job || !job.enabled) {
333
+ return;
334
+ }
335
+
336
+ const startTime = Date.now();
337
+
338
+ try {
339
+ // บันทึก log
340
+ this.logJobExecution(job, 'start');
341
+
342
+ // เรียกใช้ skill
343
+ await this.runSkill(job.skill, job.params);
344
+
345
+ // อัปเดตสถานะ
346
+ job.lastRun = new Date().toISOString();
347
+ job.nextRun = this.getNextRun(job.schedule);
348
+ job.runCount++;
349
+ job.lastDuration = Date.now() - startTime;
350
+
351
+ this.logJobExecution(job, 'success');
352
+
353
+ } catch (error) {
354
+ job.errorCount++;
355
+ job.lastError = error.message;
356
+ this.logJobExecution(job, 'error', error.message);
357
+ }
358
+
359
+ this.saveJobs(jobs);
360
+ }
361
+
362
+ /**
363
+ * รัน skill
364
+ */
365
+ async runSkill(skillName, params) {
366
+ // ในโลกจริงจะเรียก OpenClaw API
367
+ // const response = await axios.post(`${baseUrl}/api/skills/execute`, {
368
+ // name: skillName,
369
+ // params,
370
+ // });
371
+
372
+ // สำหรับตอนนี้จำลองการทำงาน
373
+ console.log(chalk.gray(`[CRON] Running ${skillName} with params:`, JSON.stringify(params)));
374
+ }
375
+
376
+ /**
377
+ * สร้าง cron script file
378
+ */
379
+ createCronScript(job) {
380
+ const scriptContent = `#!/usr/bin/env node
381
+ // Auto-generated cron script for ${job.skill}
382
+ // Generated at: ${new Date().toISOString()}
383
+
384
+ const axios = require('axios');
385
+
386
+ const skill = '${job.skill}';
387
+ const params = ${JSON.stringify(job.params, null, 2)};
388
+ const config = require('${this.configManager.getSkillsPath()}/${job.skill}.config.json');
389
+
390
+ async function run() {
391
+ try {
392
+ console.log(\`[\${new Date().toISOString()}] Running \${skill}...\`);
393
+
394
+ // Call OpenClaw API
395
+ const baseUrl = process.env.OPENCLAW_URL || 'http://localhost:3000';
396
+ const response = await axios.post(\`\${baseUrl}/api/skills/execute\`, {
397
+ name: skill,
398
+ params: { ...config, ...params },
399
+ });
400
+
401
+ console.log(\`[\${new Date().toISOString()}] Success:\`, response.data);
402
+ } catch (error) {
403
+ console.error(\`[\${new Date().toISOString()}] Error:\`, error.message);
404
+ process.exit(1);
405
+ }
406
+ }
407
+
408
+ run();
409
+ `;
410
+
411
+ const scriptPath = path.join(this.configManager.getCronsPath(), `${job.id}.js`);
412
+ fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
413
+ }
414
+
415
+ /**
416
+ * ลบ cron script file
417
+ */
418
+ removeCronScript(jobId) {
419
+ const scriptPath = path.join(this.configManager.getCronsPath(), `${jobId}.js`);
420
+ if (fs.existsSync(scriptPath)) {
421
+ fs.removeSync(scriptPath);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * บันทึก log การทำงาน
427
+ */
428
+ logJobExecution(job, status, message = '') {
429
+ const logPath = path.join(this.configManager.getLogsPath(), `cron-${new Date().toISOString().split('T')[0]}.log`);
430
+ const logEntry = `[${new Date().toISOString()}] ${status.toUpperCase()} - ${job.skill} (${job.id}): ${message}\n`;
431
+
432
+ fs.appendFileSync(logPath, logEntry);
433
+ }
434
+
435
+ /**
436
+ * คำนวณเวลารันครั้งถัดไป
437
+ */
438
+ getNextRun(_schedule) {
439
+ // ง่ายๆ แค่ return null ตอนนี้
440
+ // ในอนาคตอาจใช้ library คำนวณจริง
441
+ return null;
442
+ }
443
+
444
+ /**
445
+ * เปิด/ปิด job
446
+ */
447
+ toggleJob(jobId, enabled) {
448
+ if (this.useOpenClawCron) {
449
+ throw new Error('โหมด openclaw cron: ให้ใช้คำสั่ง "openclaw cron enable|disable <id>"');
450
+ }
451
+
452
+ const jobs = this.getJobs();
453
+ const job = jobs.find(j => j.id === jobId);
454
+
455
+ if (!job) {
456
+ throw new Error(`ไม่พบ cronjob ID: ${jobId}`);
457
+ }
458
+
459
+ job.enabled = enabled;
460
+ this.saveJobs(jobs);
461
+
462
+ if (enabled) {
463
+ this.startJob(job);
464
+ console.log(chalk.green(`✓ เปิดใช้งาน cronjob: ${job.skill}`));
465
+ } else {
466
+ this.stopJob(jobId);
467
+ console.log(chalk.yellow(`⏸️ ปิดใช้งาน cronjob: ${job.skill}`));
468
+ }
469
+
470
+ return job;
471
+ }
472
+
473
+ /**
474
+ * เริ่มต้นระบบ cron (เรียกตอน start)
475
+ */
476
+ startAll() {
477
+ if (this.useOpenClawCron) {
478
+ console.log(chalk.green('✓ ใช้ openclaw cron scheduler (ไม่ต้อง start local tasks)'));
479
+ return;
480
+ }
481
+
482
+ const jobs = this.getJobs().filter(j => j.enabled);
483
+
484
+ for (const job of jobs) {
485
+ this.startJob(job);
486
+ }
487
+
488
+ console.log(chalk.green(`✓ เริ่มต้นระบบ cron (${jobs.length} jobs)`));
489
+ }
490
+
491
+ /**
492
+ * หยุดระบบ cron ทั้งหมด
493
+ */
494
+ stopAll() {
495
+ if (this.useOpenClawCron) {
496
+ console.log(chalk.yellow('⏹️ openclaw cron scheduler ยังทำงานภายนอก process นี้'));
497
+ return;
498
+ }
499
+
500
+ for (const [, task] of this.tasks) {
501
+ task.stop();
502
+ if (typeof task.destroy === 'function') {
503
+ task.destroy();
504
+ }
505
+ }
506
+ this.tasks.clear();
507
+
508
+ console.log(chalk.yellow('⏹️ หยุดระบบ cron'));
509
+ }
510
+ }
511
+
512
+ module.exports = CronManager;