diff --git a/package.json b/package.json index f5b7c68..555ee9b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a558ed..61e6513 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/main/core/profileUpdater.ts b/src/main/core/profileUpdater.ts index dae6493..122f9b7 100644 --- a/src/main/core/profileUpdater.ts +++ b/src/main/core/profileUpdater.ts @@ -1,22 +1,37 @@ import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config' +import { Cron } from 'croner' -const intervalPool: Record = {} +const intervalPool: Record = {} export async function initProfileUpdater(): Promise { 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( - async () => { + if (typeof item.interval === 'number') { + // 数字间隔使用setInterval + intervalPool[item.id] = setInterval( + async () => { + try { + await addProfileItem(item) + } catch (e) { + /* ignore */ + } + }, + 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 */ } - }, - item.interval * 60 * 1000 - ) + }) + } + try { await addProfileItem(item) } catch (e) { @@ -24,17 +39,40 @@ export async function initProfileUpdater(): Promise { } } } + if (currentItem?.type === 'remote' && currentItem.interval) { - intervalPool[currentItem.id] = setTimeout( - async () => { + 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) + } catch (e) { + /* ignore */ + } + }, + 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 */ } - }, - currentItem.interval * 60 * 1000 + 10000 // +10s - ) + }) + } + try { await addProfileItem(currentItem) } catch (e) { @@ -46,17 +84,32 @@ export async function initProfileUpdater(): Promise { export async function addProfileUpdater(item: IProfileItem): Promise { 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( - async () => { + + if (typeof item.interval === 'number') { + intervalPool[item.id] = setInterval( + async () => { + try { + await addProfileItem(item) + } catch (e) { + /* ignore */ + } + }, + 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 */ } - }, - item.interval * 60 * 1000 - ) + }) + } } -} +} \ No newline at end of file diff --git a/src/renderer/src/components/profiles/edit-info-modal.tsx b/src/renderer/src/components/profiles/edit-info-modal.tsx index 650d0e5..f581173 100644 --- a/src/renderer/src/components/profiles/edit-info-modal.tsx +++ b/src/renderer/src/components/profiles/edit-info-modal.tsx @@ -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) => { /> - { - 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 * * * *'" + /> + + {/* 动态提示信息 */} +
+ {typeof values.interval === 'number' ? ( + '以分钟为单位的定时间隔' + ) : /^\d+$/.test(values.interval?.toString() || '') ? ( + '以分钟为单位的定时间隔' + ) : isValidCron(values.interval?.toString() || '', { seconds: false }) ? ( + '有效的Cron表达式' + ) : ( + '请输入数字或合法的Cron表达式(如:0 * * * *)' + )} +
+