diff --git a/changelog.md b/changelog.md index 0df8ddc0..b729cffc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,8 @@ -# 1.9.4 +# 1.9.5 ## 新功能 (Feat) +- 更新 mihomo 内核 - 新增每个订阅独立的 User-Agent 配置 - 使用 mshta 在 Electron 初始化前同步检测 PowerShell 版本 @@ -9,6 +10,7 @@ - 修复 Win7 兼容性问题 - 修复日志清理正则表达式以正确匹配带前缀的文件名 +- 修复订阅自动更新在异常间隔或错误配置下可能触发高频刷新请求的问题 ## 其他 (Chore) diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index 31d38f14..0928b9e3 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -258,6 +258,9 @@ interface FetchResult { headers: Record } +const MAX_TIMER_DELAY_MS = 2_147_483_647 +const MAX_PROFILE_INTERVAL_MINUTES = Math.floor(MAX_TIMER_DELAY_MS / (60 * 1000)) + async function fetchAndValidateSubscription(options: FetchOptions): Promise { const { url, useProxy, mixedPort, userAgent, authToken, timeout, substore } = options @@ -377,10 +380,9 @@ export async function createProfile(item: Partial): Promise 0) { - newItem.interval = hours * 60 + newItem.interval = Math.min(Math.ceil(hours * 60), MAX_PROFILE_INTERVAL_MINUTES) } } if (headers['subscription-userinfo']) { diff --git a/src/main/core/profileUpdater.ts b/src/main/core/profileUpdater.ts index 59bbebfc..9ff2558e 100644 --- a/src/main/core/profileUpdater.ts +++ b/src/main/core/profileUpdater.ts @@ -4,47 +4,96 @@ import { logger } from '../utils/logger' const intervalPool: Record = {} const delayedUpdatePool: Record = {} +const updatingProfileIds = new Set() // 定时触发的订阅刷新至少间隔1分钟 const MIN_INTERVAL_MS = 60 * 1000 +const MAX_TIMER_DELAY_MS = 2_147_483_647 function safeIntervalMs(minutes: unknown): number { const requestedMs = Number(minutes) * 60 * 1000 if (!Number.isFinite(requestedMs) || requestedMs <= 0) return MIN_INTERVAL_MS - return Math.max(MIN_INTERVAL_MS, requestedMs) + return Math.min(Math.max(requestedMs, MIN_INTERVAL_MS), MAX_TIMER_DELAY_MS) +} + +function intervalDelayMs(interval: unknown): number | undefined { + const minutes = Number(interval) + if (!Number.isFinite(minutes) || minutes <= 0) return undefined + return safeIntervalMs(minutes) } async function updateProfile(id: string): Promise { - const item = await getProfileItem(id) - if (item && item.type === 'remote') { - await addProfileItem(item) + if (updatingProfileIds.has(id)) return + updatingProfileIds.add(id) + try { + const item = await getProfileItem(id) + if (item && item.type === 'remote') { + await addProfileItem(item) + } + } finally { + updatingProfileIds.delete(id) } } +function updateTask(itemId: string, logLabel: string): () => Promise { + return async () => { + try { + await updateProfile(itemId) + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update ${logLabel}:`, e) + } + } +} + +function scheduleProfileUpdate(item: IProfileItem): void { + if (item.type !== 'remote' || !item.autoUpdate || !item.interval) return + + const itemId = item.id + const logLabel = `profile ${itemId}` + const delayMs = intervalDelayMs(item.interval) + if (delayMs) { + intervalPool[itemId] = setInterval(updateTask(itemId, logLabel), delayMs) + return + } + + if (typeof item.interval !== 'string') return + + const cronExpression = item.interval.trim() + // 只接受 5 段 cron;6 段 cron 带秒,会绕过 UI 造成秒级刷新。 + if (cronExpression.split(/\s+/).length !== 5) return + + try { + intervalPool[itemId] = new Cron(cronExpression, updateTask(itemId, logLabel)) + } catch { + // ignore invalid cron + } +} + +function scheduleDelayedCurrentUpdate(item: IProfileItem): void { + const delayMs = intervalDelayMs(item.interval) + if (!delayMs) return + + const itemId = item.id + delayedUpdatePool[itemId] = setTimeout( + async () => { + delete delayedUpdatePool[itemId] + try { + await updateProfile(itemId) + } catch (e) { + await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) + } + }, + Math.min(delayMs + 10000, MAX_TIMER_DELAY_MS) + ) +} + 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.autoUpdate && item.interval) { - const itemId = item.id - if (typeof item.interval === 'number') { - intervalPool[itemId] = setInterval(async () => { - try { - await updateProfile(itemId) - } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) - } - }, safeIntervalMs(item.interval)) - } else if (typeof item.interval === 'string') { - intervalPool[itemId] = new Cron(item.interval, async () => { - try { - await updateProfile(itemId) - } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) - } - }) - } + await addProfileUpdater(item) try { await addProfileItem(item) @@ -56,71 +105,22 @@ export async function initProfileUpdater(): Promise { if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) { const currentId = currentItem.id - if (typeof currentItem.interval === 'number') { - const currentMs = safeIntervalMs(currentItem.interval) - intervalPool[currentId] = setInterval(async () => { - try { - await updateProfile(currentId) - } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) - } - }, currentMs) - - delayedUpdatePool[currentId] = setTimeout(async () => { - delete delayedUpdatePool[currentId] - try { - await updateProfile(currentId) - } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) - } - }, currentMs + 10000) - } else if (typeof currentItem.interval === 'string') { - intervalPool[currentId] = new Cron(currentItem.interval, async () => { - try { - await updateProfile(currentId) - } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) - } - }) - } + await addProfileUpdater(currentItem) try { await addProfileItem(currentItem) } catch (e) { await logger.warn(`[ProfileUpdater] Failed to init current profile:`, e) } + + const latestCurrentItem = (await getProfileItem(currentId)) ?? currentItem + scheduleDelayedCurrentUpdate(latestCurrentItem) } } export async function addProfileUpdater(item: IProfileItem): Promise { - if (item.type === 'remote' && item.autoUpdate && item.interval) { - if (intervalPool[item.id]) { - if (intervalPool[item.id] instanceof Cron) { - ;(intervalPool[item.id] as Cron).stop() - } else { - clearInterval(intervalPool[item.id] as NodeJS.Timeout) - } - } - - const itemId = item.id - if (typeof item.interval === 'number') { - intervalPool[itemId] = setInterval(async () => { - try { - await updateProfile(itemId) - } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) - } - }, safeIntervalMs(item.interval)) - } else if (typeof item.interval === 'string') { - intervalPool[itemId] = new Cron(item.interval, async () => { - try { - await updateProfile(itemId) - } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) - } - }) - } - } + await removeProfileUpdater(item.id) + scheduleProfileUpdate(item) } export async function removeProfileUpdater(id: string): Promise {