Files
zn-ai/src/pages/cron/utils.ts
2026-04-11 23:17:54 +08:00

159 lines
5.2 KiB
TypeScript

/**
* Cron utilities
* Parse schedules and estimate next run times
*/
import type { CronSchedule } from '@lib/cron-types';
export type ScheduleType = 'daily' | 'weekly' | 'monthly' | 'interval' | 'custom';
export interface SchedulePreset {
key: string;
value: string;
type: ScheduleType;
}
export const schedulePresets: SchedulePreset[] = [
{ key: 'everyMinute', value: '* * * * *', type: 'interval' },
{ key: 'every5Min', value: '*/5 * * * *', type: 'interval' },
{ key: 'every15Min', value: '*/15 * * * *', type: 'interval' },
{ key: 'everyHour', value: '0 * * * *', type: 'interval' },
{ key: 'daily9am', value: '0 9 * * *', type: 'daily' },
{ key: 'daily6pm', value: '0 18 * * *', type: 'daily' },
{ key: 'weeklyMon', value: '0 9 * * 1', type: 'weekly' },
{ key: 'monthly1st', value: '0 9 1 * *', type: 'monthly' },
];
// Parse cron schedule to human-readable format
// Handles both plain cron strings and Gateway CronSchedule objects
export function parseCronSchedule(
schedule: unknown,
t: (key: string, interpolations?: Record<string, unknown>) => string,
): string {
if (schedule && typeof schedule === 'object') {
const s = schedule as { kind?: string; expr?: string; tz?: string; everyMs?: number; at?: string };
if (s.kind === 'cron' && typeof s.expr === 'string') {
return parseCronExpr(s.expr, t);
}
if (s.kind === 'every' && typeof s.everyMs === 'number') {
const ms = s.everyMs;
if (ms < 60_000) return t('cron.schedule.everySeconds', { count: Math.round(ms / 1000) });
if (ms < 3_600_000) return t('cron.schedule.everyMinutes', { count: Math.round(ms / 60_000) });
if (ms < 86_400_000) return t('cron.schedule.everyHours', { count: Math.round(ms / 3_600_000) });
return t('cron.schedule.everyDays', { count: Math.round(ms / 86_400_000) });
}
if (s.kind === 'at' && typeof s.at === 'string') {
try {
return t('cron.schedule.onceAt', { time: new Date(s.at).toLocaleString() });
} catch {
return t('cron.schedule.onceAt', { time: s.at });
}
}
return String(schedule);
}
if (typeof schedule === 'string') {
return parseCronExpr(schedule, t);
}
return String(schedule ?? t('cron.schedule.unknown'));
}
export function parseCronExpr(
cron: string,
t: (key: string, interpolations?: Record<string, unknown>) => string,
): string {
const preset = schedulePresets.find((p) => p.value === cron);
if (preset) return t(`cron.presets.${preset.key}` as const);
const parts = cron.split(' ');
if (parts.length !== 5) return cron;
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
if (minute === '*' && hour === '*') return t('cron.presets.everyMinute');
if (minute.startsWith('*/')) return t('cron.schedule.everyMinutes', { count: Number(minute.slice(2)) });
if (hour === '*' && minute === '0') return t('cron.presets.everyHour');
if (dayOfWeek !== '*' && dayOfMonth === '*') {
return t('cron.schedule.weeklyAt', { day: dayOfWeek, time: `${hour}:${minute.padStart(2, '0')}` });
}
if (dayOfMonth !== '*') {
return t('cron.schedule.monthlyAtDay', { day: dayOfMonth, time: `${hour}:${minute.padStart(2, '0')}` });
}
if (hour !== '*') {
return t('cron.schedule.dailyAt', { time: `${hour}:${minute.padStart(2, '0')}` });
}
return cron;
}
export function estimateNextRun(scheduleExpr: string): string | null {
const now = new Date();
const next = new Date(now.getTime());
if (scheduleExpr === '* * * * *') {
next.setSeconds(0, 0);
next.setMinutes(next.getMinutes() + 1);
return formatLocaleDateTime(next);
}
if (scheduleExpr === '*/5 * * * *') {
const delta = 5 - (next.getMinutes() % 5 || 5);
next.setSeconds(0, 0);
next.setMinutes(next.getMinutes() + delta);
return formatLocaleDateTime(next);
}
if (scheduleExpr === '*/15 * * * *') {
const delta = 15 - (next.getMinutes() % 15 || 15);
next.setSeconds(0, 0);
next.setMinutes(next.getMinutes() + delta);
return formatLocaleDateTime(next);
}
if (scheduleExpr === '0 * * * *') {
next.setMinutes(0, 0, 0);
next.setHours(next.getHours() + 1);
return formatLocaleDateTime(next);
}
if (scheduleExpr === '0 9 * * *' || scheduleExpr === '0 18 * * *') {
const targetHour = scheduleExpr === '0 9 * * *' ? 9 : 18;
next.setSeconds(0, 0);
next.setHours(targetHour, 0, 0, 0);
if (next <= now) next.setDate(next.getDate() + 1);
return formatLocaleDateTime(next);
}
if (scheduleExpr === '0 9 * * 1') {
next.setSeconds(0, 0);
next.setHours(9, 0, 0, 0);
const day = next.getDay();
const daysUntilMonday = day === 1 ? 7 : (8 - day) % 7;
next.setDate(next.getDate() + daysUntilMonday);
return formatLocaleDateTime(next);
}
if (scheduleExpr === '0 9 1 * *') {
next.setSeconds(0, 0);
next.setDate(1);
next.setHours(9, 0, 0, 0);
if (next <= now) next.setMonth(next.getMonth() + 1);
return formatLocaleDateTime(next);
}
return null;
}
function formatLocaleDateTime(date: Date): string {
try {
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
} catch {
return date.toLocaleString();
}
}