mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
feat: support cron in profile update interval (#964)
This commit is contained in:
parent
e20e46aebe
commit
c506d60e66
@ -33,6 +33,7 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
|
"croner": "^9.1.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@ -38,6 +38,9 @@ importers:
|
|||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^4.0.3
|
specifier: ^4.0.3
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
|
croner:
|
||||||
|
specifier: ^9.1.0
|
||||||
|
version: 9.1.0
|
||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@ -2650,6 +2653,10 @@ packages:
|
|||||||
cron-validator@1.4.0:
|
cron-validator@1.4.0:
|
||||||
resolution: {integrity: sha512-wGcJ9FCy65iaU6egSH8b5dZYJF7GU/3Jh06wzaT9lsa5dbqExjljmu+0cJ8cpKn+vUyZa/EM4WAxeLR6SypJXw==}
|
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:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -8573,6 +8580,8 @@ snapshots:
|
|||||||
|
|
||||||
cron-validator@1.4.0: {}
|
cron-validator@1.4.0: {}
|
||||||
|
|
||||||
|
croner@9.1.0: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
|
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> {
|
export async function initProfileUpdater(): Promise<void> {
|
||||||
const { items, current } = await getProfileConfig()
|
const { items, current } = await getProfileConfig()
|
||||||
const currentItem = await getCurrentProfileItem()
|
const currentItem = await getCurrentProfileItem()
|
||||||
|
|
||||||
for (const item of items.filter((i) => i.id !== current)) {
|
for (const item of items.filter((i) => i.id !== current)) {
|
||||||
if (item.type === 'remote' && item.interval) {
|
if (item.type === 'remote' && item.interval) {
|
||||||
intervalPool[item.id] = setTimeout(
|
if (typeof item.interval === 'number') {
|
||||||
|
// 数字间隔使用setInterval
|
||||||
|
intervalPool[item.id] = setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await addProfileItem(item)
|
||||||
@ -17,6 +21,17 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
},
|
},
|
||||||
item.interval * 60 * 1000
|
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 {
|
try {
|
||||||
await addProfileItem(item)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -24,8 +39,21 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentItem?.type === 'remote' && currentItem.interval) {
|
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 () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(currentItem)
|
await addProfileItem(currentItem)
|
||||||
@ -35,6 +63,16 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
},
|
},
|
||||||
currentItem.interval * 60 * 1000 + 10000 // +10s
|
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 {
|
try {
|
||||||
await addProfileItem(currentItem)
|
await addProfileItem(currentItem)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -46,9 +84,15 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
export async function addProfileUpdater(item: IProfileItem): Promise<void> {
|
export async function addProfileUpdater(item: IProfileItem): Promise<void> {
|
||||||
if (item.type === 'remote' && item.interval) {
|
if (item.type === 'remote' && item.interval) {
|
||||||
if (intervalPool[item.id]) {
|
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 () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await addProfileItem(item)
|
||||||
@ -58,5 +102,14 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
|
|||||||
},
|
},
|
||||||
item.interval * 60 * 1000
|
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 */
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -20,6 +20,7 @@ import { restartCore, addProfileUpdater } from '@renderer/utils/ipc'
|
|||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md'
|
||||||
import { FaPlus } from 'react-icons/fa6'
|
import { FaPlus } from 'react-icons/fa6'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { isValidCron } from 'cron-validator';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: IProfileItem
|
item: IProfileItem
|
||||||
@ -100,15 +101,57 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title={t('profiles.editInfo.interval')}>
|
<SettingItem title={t('profiles.editInfo.interval')}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
type="number"
|
type="text"
|
||||||
className={cn(inputWidth)}
|
className={cn(
|
||||||
|
inputWidth,
|
||||||
|
// 不合法
|
||||||
|
typeof values.interval === 'string' &&
|
||||||
|
!/^\d+$/.test(values.interval) &&
|
||||||
|
!isValidCron(values.interval, { seconds: false }) &&
|
||||||
|
'border-red-500'
|
||||||
|
)}
|
||||||
value={values.interval?.toString() ?? ''}
|
value={values.interval?.toString() ?? ''}
|
||||||
onValueChange={(v) => {
|
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>
|
||||||
<SettingItem title={t('profiles.editInfo.fixedInterval')}>
|
<SettingItem title={t('profiles.editInfo.fixedInterval')}>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@ -374,7 +374,7 @@
|
|||||||
"profiles.editInfo.name": "Name",
|
"profiles.editInfo.name": "Name",
|
||||||
"profiles.editInfo.url": "Subscription URL",
|
"profiles.editInfo.url": "Subscription URL",
|
||||||
"profiles.editInfo.useProxy": "Use Proxy to Update",
|
"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.fixedInterval": "Fixed Update Interval",
|
||||||
"profiles.editInfo.override.title": "Override",
|
"profiles.editInfo.override.title": "Override",
|
||||||
"profiles.editInfo.override.global": "Global",
|
"profiles.editInfo.override.global": "Global",
|
||||||
|
|||||||
@ -364,7 +364,7 @@
|
|||||||
"profiles.editInfo.name": "نام",
|
"profiles.editInfo.name": "نام",
|
||||||
"profiles.editInfo.url": "آدرس اشتراک",
|
"profiles.editInfo.url": "آدرس اشتراک",
|
||||||
"profiles.editInfo.useProxy": "استفاده از پراکسی برای بهروزرسانی",
|
"profiles.editInfo.useProxy": "استفاده از پراکسی برای بهروزرسانی",
|
||||||
"profiles.editInfo.interval": "فاصله بهروزرسانی (دقیقه)",
|
"profiles.editInfo.interval": "فاصله بهروزرسانی",
|
||||||
"profiles.editInfo.fixedInterval": "فاصله بهروزرسانی ثابت",
|
"profiles.editInfo.fixedInterval": "فاصله بهروزرسانی ثابت",
|
||||||
"profiles.editInfo.override.title": "جایگزینی",
|
"profiles.editInfo.override.title": "جایگزینی",
|
||||||
"profiles.editInfo.override.global": "جهانی",
|
"profiles.editInfo.override.global": "جهانی",
|
||||||
|
|||||||
@ -364,7 +364,7 @@
|
|||||||
"profiles.editInfo.name": "Имя",
|
"profiles.editInfo.name": "Имя",
|
||||||
"profiles.editInfo.url": "URL подписки",
|
"profiles.editInfo.url": "URL подписки",
|
||||||
"profiles.editInfo.useProxy": "Использовать прокси для обновления",
|
"profiles.editInfo.useProxy": "Использовать прокси для обновления",
|
||||||
"profiles.editInfo.interval": "Интервал обн. (мин)",
|
"profiles.editInfo.interval": "Интервал обн.",
|
||||||
"profiles.editInfo.fixedInterval": "Фиксированный интервал обновления",
|
"profiles.editInfo.fixedInterval": "Фиксированный интервал обновления",
|
||||||
"profiles.editInfo.override.title": "Переопределение",
|
"profiles.editInfo.override.title": "Переопределение",
|
||||||
"profiles.editInfo.override.global": "Глобальный",
|
"profiles.editInfo.override.global": "Глобальный",
|
||||||
|
|||||||
@ -379,7 +379,7 @@
|
|||||||
"profiles.editInfo.name": "名称",
|
"profiles.editInfo.name": "名称",
|
||||||
"profiles.editInfo.url": "订阅地址",
|
"profiles.editInfo.url": "订阅地址",
|
||||||
"profiles.editInfo.useProxy": "使用代理更新",
|
"profiles.editInfo.useProxy": "使用代理更新",
|
||||||
"profiles.editInfo.interval": "更新间隔(分钟)",
|
"profiles.editInfo.interval": "更新间隔",
|
||||||
"profiles.editInfo.fixedInterval": "固定更新间隔",
|
"profiles.editInfo.fixedInterval": "固定更新间隔",
|
||||||
"profiles.editInfo.override.title": "覆写",
|
"profiles.editInfo.override.title": "覆写",
|
||||||
"profiles.editInfo.override.global": "全局",
|
"profiles.editInfo.override.global": "全局",
|
||||||
|
|||||||
2
src/shared/types.d.ts
vendored
2
src/shared/types.d.ts
vendored
@ -461,7 +461,7 @@ interface IProfileItem {
|
|||||||
name: string
|
name: string
|
||||||
url?: string // remote
|
url?: string // remote
|
||||||
file?: string // local
|
file?: string // local
|
||||||
interval?: number
|
interval?: number | string
|
||||||
home?: string
|
home?: string
|
||||||
updated?: number
|
updated?: number
|
||||||
override?: string[]
|
override?: string[]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user