mirror of https://github.com/langgenius/dify.git
230 lines
7.6 KiB
TypeScript
230 lines
7.6 KiB
TypeScript
const matchesField = (value: number, pattern: string, min: number, max: number): boolean => {
|
|
if (pattern === '*') return true
|
|
|
|
if (pattern.includes(','))
|
|
return pattern.split(',').some(p => matchesField(value, p.trim(), min, max))
|
|
|
|
if (pattern.includes('/')) {
|
|
const [range, step] = pattern.split('/')
|
|
const stepValue = Number.parseInt(step, 10)
|
|
if (Number.isNaN(stepValue)) return false
|
|
|
|
if (range === '*') {
|
|
return value % stepValue === min % stepValue
|
|
}
|
|
else {
|
|
const rangeStart = Number.parseInt(range, 10)
|
|
if (Number.isNaN(rangeStart)) return false
|
|
return value >= rangeStart && (value - rangeStart) % stepValue === 0
|
|
}
|
|
}
|
|
|
|
if (pattern.includes('-')) {
|
|
const [start, end] = pattern.split('-').map(p => Number.parseInt(p.trim(), 10))
|
|
if (Number.isNaN(start) || Number.isNaN(end)) return false
|
|
return value >= start && value <= end
|
|
}
|
|
|
|
const numValue = Number.parseInt(pattern, 10)
|
|
if (Number.isNaN(numValue)) return false
|
|
return value === numValue
|
|
}
|
|
|
|
const expandCronField = (field: string, min: number, max: number): number[] => {
|
|
if (field === '*')
|
|
return Array.from({ length: max - min + 1 }, (_, i) => min + i)
|
|
|
|
if (field.includes(','))
|
|
return field.split(',').flatMap(p => expandCronField(p.trim(), min, max))
|
|
|
|
if (field.includes('/')) {
|
|
const [range, step] = field.split('/')
|
|
const stepValue = Number.parseInt(step, 10)
|
|
if (Number.isNaN(stepValue)) return []
|
|
|
|
const baseValues = range === '*' ? [min] : expandCronField(range, min, max)
|
|
const result: number[] = []
|
|
|
|
for (let start = baseValues[0]; start <= max; start += stepValue) {
|
|
if (start >= min && start <= max)
|
|
result.push(start)
|
|
}
|
|
return result
|
|
}
|
|
|
|
if (field.includes('-')) {
|
|
const [start, end] = field.split('-').map(p => Number.parseInt(p.trim(), 10))
|
|
if (Number.isNaN(start) || Number.isNaN(end)) return []
|
|
|
|
const result: number[] = []
|
|
for (let i = start; i <= end && i <= max; i++)
|
|
if (i >= min) result.push(i)
|
|
|
|
return result
|
|
}
|
|
|
|
const numValue = Number.parseInt(field, 10)
|
|
return !Number.isNaN(numValue) && numValue >= min && numValue <= max ? [numValue] : []
|
|
}
|
|
|
|
const matchesCron = (
|
|
date: Date,
|
|
minute: string,
|
|
hour: string,
|
|
dayOfMonth: string,
|
|
month: string,
|
|
dayOfWeek: string,
|
|
): boolean => {
|
|
const currentMinute = date.getMinutes()
|
|
const currentHour = date.getHours()
|
|
const currentDay = date.getDate()
|
|
const currentMonth = date.getMonth() + 1
|
|
const currentDayOfWeek = date.getDay()
|
|
|
|
// Basic time matching
|
|
if (!matchesField(currentMinute, minute, 0, 59)) return false
|
|
if (!matchesField(currentHour, hour, 0, 23)) return false
|
|
if (!matchesField(currentMonth, month, 1, 12)) return false
|
|
|
|
// Day matching logic: if both dayOfMonth and dayOfWeek are specified (not *),
|
|
// the cron should match if EITHER condition is true (OR logic)
|
|
const dayOfMonthSpecified = dayOfMonth !== '*'
|
|
const dayOfWeekSpecified = dayOfWeek !== '*'
|
|
|
|
if (dayOfMonthSpecified && dayOfWeekSpecified) {
|
|
// If both are specified, match if either matches
|
|
return matchesField(currentDay, dayOfMonth, 1, 31)
|
|
|| matchesField(currentDayOfWeek, dayOfWeek, 0, 6)
|
|
}
|
|
else if (dayOfMonthSpecified) {
|
|
// Only day of month specified
|
|
return matchesField(currentDay, dayOfMonth, 1, 31)
|
|
}
|
|
else if (dayOfWeekSpecified) {
|
|
// Only day of week specified
|
|
return matchesField(currentDayOfWeek, dayOfWeek, 0, 6)
|
|
}
|
|
else {
|
|
// Both are *, matches any day
|
|
return true
|
|
}
|
|
}
|
|
|
|
export const parseCronExpression = (cronExpression: string, timezone: string = 'UTC'): Date[] => {
|
|
if (!cronExpression || cronExpression.trim() === '')
|
|
return []
|
|
|
|
const parts = cronExpression.trim().split(/\s+/)
|
|
if (parts.length !== 5)
|
|
return []
|
|
|
|
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts
|
|
|
|
try {
|
|
const nextTimes: Date[] = []
|
|
|
|
// Get user timezone current time - no browser timezone involved
|
|
const now = new Date()
|
|
const userTimeStr = now.toLocaleString('en-CA', {
|
|
timeZone: timezone,
|
|
hour12: false,
|
|
})
|
|
const [dateStr, timeStr] = userTimeStr.split(', ')
|
|
const [year, monthNum, day] = dateStr.split('-').map(Number)
|
|
const [nowHour, nowMinute, nowSecond] = timeStr.split(':').map(Number)
|
|
const userToday = new Date(year, monthNum - 1, day, 0, 0, 0, 0)
|
|
const userCurrentTime = new Date(year, monthNum - 1, day, nowHour, nowMinute, nowSecond)
|
|
|
|
const isMonthlyPattern = dayOfMonth !== '*' && dayOfWeek === '*'
|
|
const isWeeklyPattern = dayOfMonth === '*' && dayOfWeek !== '*'
|
|
|
|
let searchMonths = 12
|
|
if (isWeeklyPattern) searchMonths = 3
|
|
else if (!isMonthlyPattern) searchMonths = 2
|
|
|
|
for (let monthOffset = 0; monthOffset < searchMonths && nextTimes.length < 5; monthOffset++) {
|
|
const checkMonth = new Date(userToday.getFullYear(), userToday.getMonth() + monthOffset, 1)
|
|
const daysInMonth = new Date(checkMonth.getFullYear(), checkMonth.getMonth() + 1, 0).getDate()
|
|
|
|
for (let day = 1; day <= daysInMonth && nextTimes.length < 5; day++) {
|
|
const checkDate = new Date(checkMonth.getFullYear(), checkMonth.getMonth(), day)
|
|
|
|
if (minute !== '*' && hour !== '*') {
|
|
const minuteValues = expandCronField(minute, 0, 59)
|
|
const hourValues = expandCronField(hour, 0, 23)
|
|
|
|
for (const h of hourValues) {
|
|
for (const m of minuteValues) {
|
|
checkDate.setHours(h, m, 0, 0)
|
|
|
|
// Only add if execution time is in the future and matches cron pattern
|
|
if (checkDate > userCurrentTime && matchesCron(checkDate, minute, hour, dayOfMonth, month, dayOfWeek))
|
|
nextTimes.push(new Date(checkDate))
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (let h = 0; h < 24 && nextTimes.length < 5; h++) {
|
|
for (let m = 0; m < 60 && nextTimes.length < 5; m++) {
|
|
checkDate.setHours(h, m, 0, 0)
|
|
|
|
// Only add if execution time is in the future and matches cron pattern
|
|
if (checkDate > userCurrentTime && matchesCron(checkDate, minute, hour, dayOfMonth, month, dayOfWeek))
|
|
nextTimes.push(new Date(checkDate))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nextTimes.sort((a, b) => a.getTime() - b.getTime()).slice(0, 5)
|
|
}
|
|
catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
const isValidCronField = (field: string, min: number, max: number): boolean => {
|
|
if (field === '*') return true
|
|
|
|
if (field.includes(','))
|
|
return field.split(',').every(p => isValidCronField(p.trim(), min, max))
|
|
|
|
if (field.includes('/')) {
|
|
const [range, step] = field.split('/')
|
|
const stepValue = Number.parseInt(step, 10)
|
|
if (Number.isNaN(stepValue) || stepValue <= 0) return false
|
|
|
|
if (range === '*') return true
|
|
return isValidCronField(range, min, max)
|
|
}
|
|
|
|
if (field.includes('-')) {
|
|
const [start, end] = field.split('-').map(p => Number.parseInt(p.trim(), 10))
|
|
if (Number.isNaN(start) || Number.isNaN(end)) return false
|
|
return start >= min && end <= max && start <= end
|
|
}
|
|
|
|
const numValue = Number.parseInt(field, 10)
|
|
return !Number.isNaN(numValue) && numValue >= min && numValue <= max
|
|
}
|
|
|
|
export const isValidCronExpression = (cronExpression: string): boolean => {
|
|
if (!cronExpression || cronExpression.trim() === '')
|
|
return false
|
|
|
|
const parts = cronExpression.trim().split(/\s+/)
|
|
if (parts.length !== 5)
|
|
return false
|
|
|
|
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts
|
|
|
|
return (
|
|
isValidCronField(minute, 0, 59)
|
|
&& isValidCronField(hour, 0, 23)
|
|
&& isValidCronField(dayOfMonth, 1, 31)
|
|
&& isValidCronField(month, 1, 12)
|
|
&& isValidCronField(dayOfWeek, 0, 6)
|
|
)
|
|
}
|