feat: support cron in profile update interval (#964)

This commit is contained in:
Memory 2025-08-16 19:56:39 +08:00 committed by GitHub
parent e20e46aebe
commit c506d60e66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 138 additions and 32 deletions

View File

@ -33,6 +33,7 @@
"axios": "^1.11.0",
"chart.js": "^4.5.0",
"chokidar": "^4.0.3",
"croner": "^9.1.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"express": "^5.1.0",

9
pnpm-lock.yaml generated
View File

@ -38,6 +38,9 @@ importers:
chokidar:
specifier: ^4.0.3
version: 4.0.3
croner:
specifier: ^9.1.0
version: 9.1.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
@ -2650,6 +2653,10 @@ packages:
cron-validator@1.4.0:
resolution: {integrity: sha512-wGcJ9FCy65iaU6egSH8b5dZYJF7GU/3Jh06wzaT9lsa5dbqExjljmu+0cJ8cpKn+vUyZa/EM4WAxeLR6SypJXw==}
croner@9.1.0:
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
engines: {node: '>=18.0'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -8573,6 +8580,8 @@ snapshots:
cron-validator@1.4.0: {}
croner@9.1.0: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1

View File

@ -1,13 +1,17 @@
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
import { Cron } from 'croner'
const intervalPool: Record<string, NodeJS.Timeout> = {}
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
export async function initProfileUpdater(): Promise<void> {
const { items, current } = await getProfileConfig()
const currentItem = await getCurrentProfileItem()
for (const item of items.filter((i) => i.id !== current)) {
if (item.type === 'remote' && item.interval) {
intervalPool[item.id] = setTimeout(
if (typeof item.interval === 'number') {
// 数字间隔使用setInterval
intervalPool[item.id] = setInterval(
async () => {
try {
await addProfileItem(item)
@ -17,6 +21,17 @@ export async function initProfileUpdater(): Promise<void> {
},
item.interval * 60 * 1000
)
} else if (typeof item.interval === 'string') {
// 字符串间隔使用Cron
intervalPool[item.id] = new Cron(item.interval, async () => {
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
})
}
try {
await addProfileItem(item)
} catch (e) {
@ -24,8 +39,21 @@ export async function initProfileUpdater(): Promise<void> {
}
}
}
if (currentItem?.type === 'remote' && currentItem.interval) {
intervalPool[currentItem.id] = setTimeout(
if (typeof currentItem.interval === 'number') {
intervalPool[currentItem.id] = setInterval(
async () => {
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
},
currentItem.interval * 60 * 1000
)
setTimeout(
async () => {
try {
await addProfileItem(currentItem)
@ -35,6 +63,16 @@ export async function initProfileUpdater(): Promise<void> {
},
currentItem.interval * 60 * 1000 + 10000 // +10s
)
} else if (typeof currentItem.interval === 'string') {
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
})
}
try {
await addProfileItem(currentItem)
} catch (e) {
@ -46,9 +84,15 @@ export async function initProfileUpdater(): Promise<void> {
export async function addProfileUpdater(item: IProfileItem): Promise<void> {
if (item.type === 'remote' && item.interval) {
if (intervalPool[item.id]) {
clearTimeout(intervalPool[item.id])
if (intervalPool[item.id] instanceof Cron) {
(intervalPool[item.id] as Cron).stop()
} else {
clearInterval(intervalPool[item.id] as NodeJS.Timeout)
}
intervalPool[item.id] = setTimeout(
}
if (typeof item.interval === 'number') {
intervalPool[item.id] = setInterval(
async () => {
try {
await addProfileItem(item)
@ -58,5 +102,14 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
},
item.interval * 60 * 1000
)
} else if (typeof item.interval === 'string') {
intervalPool[item.id] = new Cron(item.interval, async () => {
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
})
}
}
}

View File

@ -20,6 +20,7 @@ import { restartCore, addProfileUpdater } from '@renderer/utils/ipc'
import { MdDeleteForever } from 'react-icons/md'
import { FaPlus } from 'react-icons/fa6'
import { useTranslation } from 'react-i18next'
import { isValidCron } from 'cron-validator';
interface Props {
item: IProfileItem
@ -100,15 +101,57 @@ const EditInfoModal: React.FC<Props> = (props) => {
/>
</SettingItem>
<SettingItem title={t('profiles.editInfo.interval')}>
<div className="flex flex-col gap-2">
<Input
size="sm"
type="number"
className={cn(inputWidth)}
type="text"
className={cn(
inputWidth,
// 不合法
typeof values.interval === 'string' &&
!/^\d+$/.test(values.interval) &&
!isValidCron(values.interval, { seconds: false }) &&
'border-red-500'
)}
value={values.interval?.toString() ?? ''}
onValueChange={(v) => {
setValues({ ...values, interval: parseInt(v) })
// 输入限制
if (/^[\d\s*\-,\/]*$/.test(v)) {
// 纯数字
if (/^\d+$/.test(v)) {
setValues({ ...values, interval: parseInt(v, 10) || 0 });
return;
}
// 非纯数字
try {
setValues({ ...values, interval: v });
} catch (e) {
// ignore
}
}
}}
placeholder="例如30 或 '0 * * * *'"
/>
{/* 动态提示信息 */}
<div className="text-xs" style={{
color: typeof values.interval === 'string' &&
!/^\d+$/.test(values.interval) &&
!isValidCron(values.interval, { seconds: false })
? '#ef4444'
: '#6b7280'
}}>
{typeof values.interval === 'number' ? (
'以分钟为单位的定时间隔'
) : /^\d+$/.test(values.interval?.toString() || '') ? (
'以分钟为单位的定时间隔'
) : isValidCron(values.interval?.toString() || '', { seconds: false }) ? (
'有效的Cron表达式'
) : (
'请输入数字或合法的Cron表达式0 * * * *'
)}
</div>
</div>
</SettingItem>
<SettingItem title={t('profiles.editInfo.fixedInterval')}>
<Switch

View File

@ -374,7 +374,7 @@
"profiles.editInfo.name": "Name",
"profiles.editInfo.url": "Subscription URL",
"profiles.editInfo.useProxy": "Use Proxy to Update",
"profiles.editInfo.interval": "Upd. Interval (min)",
"profiles.editInfo.interval": "Upd. Interval",
"profiles.editInfo.fixedInterval": "Fixed Update Interval",
"profiles.editInfo.override.title": "Override",
"profiles.editInfo.override.global": "Global",

View File

@ -364,7 +364,7 @@
"profiles.editInfo.name": "نام",
"profiles.editInfo.url": "آدرس اشتراک",
"profiles.editInfo.useProxy": "استفاده از پراکسی برای به‌روزرسانی",
"profiles.editInfo.interval": "فاصله به‌روزرسانی (دقیقه)",
"profiles.editInfo.interval": "فاصله به‌روزرسانی",
"profiles.editInfo.fixedInterval": "فاصله به‌روزرسانی ثابت",
"profiles.editInfo.override.title": "جایگزینی",
"profiles.editInfo.override.global": "جهانی",

View File

@ -364,7 +364,7 @@
"profiles.editInfo.name": "Имя",
"profiles.editInfo.url": "URL подписки",
"profiles.editInfo.useProxy": "Использовать прокси для обновления",
"profiles.editInfo.interval": "Интервал обн. (мин)",
"profiles.editInfo.interval": "Интервал обн.",
"profiles.editInfo.fixedInterval": "Фиксированный интервал обновления",
"profiles.editInfo.override.title": "Переопределение",
"profiles.editInfo.override.global": "Глобальный",

View File

@ -379,7 +379,7 @@
"profiles.editInfo.name": "名称",
"profiles.editInfo.url": "订阅地址",
"profiles.editInfo.useProxy": "使用代理更新",
"profiles.editInfo.interval": "更新间隔(分钟)",
"profiles.editInfo.interval": "更新间隔",
"profiles.editInfo.fixedInterval": "固定更新间隔",
"profiles.editInfo.override.title": "覆写",
"profiles.editInfo.override.global": "全局",

View File

@ -461,7 +461,7 @@ interface IProfileItem {
name: string
url?: string // remote
file?: string // local
interval?: number
interval?: number | string
home?: string
updated?: number
override?: string[]