From d3a23a060101ceaf1f4c2e80f647b4ced74d79de Mon Sep 17 00:00:00 2001 From: julong <129662742+julong111@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:42:45 +0800 Subject: [PATCH] feat: subscription timeout settings & subscription update logic efficiency (#1562) 1. Add updateTimeout property to IProfileItem interface 2. Add dedicated timeout config option for subscriptions 3. Improve subscription update logic efficiency 4. Use subscriptionTimeout as the smart fallback time 5. Localize profiles.editInfo.updateTimeout and profiles.editInfo.updateTimeoutPlaceholder for en-US/fa-IR/ru-RU/zh-CN/zh-TW --- src/main/config/profile.ts | 125 ++++++++---------- .../components/profiles/edit-info-modal.tsx | 24 +++- src/renderer/src/locales/en-US.json | 2 + src/renderer/src/locales/fa-IR.json | 2 + src/renderer/src/locales/ru-RU.json | 2 + src/renderer/src/locales/zh-CN.json | 2 + src/renderer/src/locales/zh-TW.json | 2 + src/shared/types.d.ts | 1 + 8 files changed, 87 insertions(+), 73 deletions(-) diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index 9dae81c..68b2643 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -218,10 +218,10 @@ async function fetchAndValidateSubscription(options: FetchOptions): Promise): Promise { const id = item.id || new Date().getTime().toString(16) - const newItem = { + const newItem: IProfileItem = { id, name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'), - type: item.type, + type: item.type!, url: item.url, substore: item.substore || false, interval: item.interval || 0, @@ -230,85 +230,66 @@ export async function createProfile(item: Partial): Promise = { - url: item.url, - mixedPort, - userAgent: userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`, - authToken: item.authToken, - substore: newItem.substore || false - } + // Remote + if (!item.url) throw new Error('Empty URL') - let result: FetchResult - let finalUseProxy = newItem.useProxy + const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig() + const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig() + const userItemTimeoutMs = (newItem.updateTimeout || 5) * 1000 - if (newItem.useProxy) { - result = await fetchAndValidateSubscription({ - ...baseOptions, - useProxy: true, - timeout: subscriptionTimeout - }) - } else if (newItem.substore) { - // SubStore requests (especially collections) need more time as they fetch and merge multiple subscriptions - // Use the full subscriptionTimeout since SubStore is a local server and doesn't need smart fallback - result = await fetchAndValidateSubscription({ - ...baseOptions, - useProxy: false, - timeout: subscriptionTimeout - }) - } else { - const smartTimeout = 5000 - try { - result = await fetchAndValidateSubscription({ - ...baseOptions, - useProxy: false, - timeout: smartTimeout - }) - } catch (directError) { - try { - result = await fetchAndValidateSubscription({ - ...baseOptions, - useProxy: true, - timeout: smartTimeout - }) - finalUseProxy = true - } catch { - throw directError - } - } - } + const baseOptions: Omit = { + url: item.url, + mixedPort, + userAgent: userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`, + authToken: item.authToken, + substore: newItem.substore || false + } - newItem.useProxy = finalUseProxy - const { data, headers } = result + const fetchSub = (useProxy: boolean, timeout: number) => + fetchAndValidateSubscription({ ...baseOptions, useProxy, timeout }) - if (headers['content-disposition'] && newItem.name === 'Remote File') { - newItem.name = parseFilename(headers['content-disposition']) + let result: FetchResult + if (newItem.useProxy || newItem.substore) { + result = await fetchSub(newItem.useProxy!, userItemTimeoutMs) + } else { + try { + result = await fetchSub(false, userItemTimeoutMs) + } catch (directError) { + try { + // smart fallback + result = await fetchSub(true, subscriptionTimeout) + } catch { + throw directError } - if (headers['profile-web-page-url']) { - newItem.home = headers['profile-web-page-url'] - } - if (headers['profile-update-interval'] && !item.allowFixedInterval) { - newItem.interval = parseInt(headers['profile-update-interval']) * 60 - } - if (headers['subscription-userinfo']) { - newItem.extra = parseSubinfo(headers['subscription-userinfo']) - } - await setProfileStr(id, data) - break - } - case 'local': { - await setProfileStr(id, item.file || '') - break } } + + const { data, headers } = result + + if (headers['content-disposition'] && newItem.name === 'Remote File') { + newItem.name = parseFilename(headers['content-disposition']) + } + if (headers['profile-web-page-url']) { + newItem.home = headers['profile-web-page-url'] + } + if (headers['profile-update-interval'] && !item.allowFixedInterval) { + newItem.interval = parseInt(headers['profile-update-interval']) * 60 + } + if (headers['subscription-userinfo']) { + newItem.extra = parseSubinfo(headers['subscription-userinfo']) + } + + await setProfileStr(id, data) return newItem } diff --git a/src/renderer/src/components/profiles/edit-info-modal.tsx b/src/renderer/src/components/profiles/edit-info-modal.tsx index 54cbcbc..e0d3082 100644 --- a/src/renderer/src/components/profiles/edit-info-modal.tsx +++ b/src/renderer/src/components/profiles/edit-info-modal.tsx @@ -32,7 +32,10 @@ const EditInfoModal: React.FC = (props) => { const { item, updateProfileItem, onClose } = props const { overrideConfig } = useOverrideConfig() const { items: overrideItems = [] } = overrideConfig || {} - const [values, setValues] = useState(item) + const [values, setValues] = useState({ + ...item, + updateTimeout: item.updateTimeout ?? 5 + }) const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]' const { t } = useTranslation() @@ -40,6 +43,7 @@ const EditInfoModal: React.FC = (props) => { try { const updatedItem = { ...values, + updateTimeout: values.updateTimeout ?? 5, override: values.override?.filter( (i) => overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global @@ -192,6 +196,24 @@ const EditInfoModal: React.FC = (props) => { )} )} + + { + if (v === '') { + setValues({ ...values, updateTimeout: undefined as unknown as number }) + return + } + if (/^\d+$/.test(v)) { + setValues({ ...values, updateTimeout: parseInt(v, 10) }) + } + }} + placeholder={t('profiles.editInfo.updateTimeoutPlaceholder')} + /> +
{overrideItems diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json index ce1c916..4f50eea 100644 --- a/src/renderer/src/locales/en-US.json +++ b/src/renderer/src/locales/en-US.json @@ -484,6 +484,8 @@ "profiles.editInfo.override.global": "Global", "profiles.editInfo.override.noAvailable": "No available overrides", "profiles.editInfo.override.add": "Add Override", + "profiles.editInfo.updateTimeout": "Update Timeout", + "profiles.editInfo.updateTimeoutPlaceholder": "Timeout in seconds", "profiles.editFile.title": "Edit Profile", "profiles.editFile.notice": "Note: Changes made here will be reset after profile update. For custom configurations, please use", "profiles.editFile.override": "Override", diff --git a/src/renderer/src/locales/fa-IR.json b/src/renderer/src/locales/fa-IR.json index a986340..585edb3 100644 --- a/src/renderer/src/locales/fa-IR.json +++ b/src/renderer/src/locales/fa-IR.json @@ -454,6 +454,8 @@ "profiles.editInfo.override.global": "جهانی", "profiles.editInfo.override.noAvailable": "جایگزینی‌ای در دسترس نیست", "profiles.editInfo.override.add": "افزودن جایگزینی", + "profiles.editInfo.updateTimeout": "زمان انتظار به‌روزرسانی", + "profiles.editInfo.updateTimeoutPlaceholder": "زمان پایان دادن به ثانیه", "profiles.editInfo.intervalPlaceholder": "مثال: 30 یا '0 * * * *'", "profiles.editInfo.intervalInvalid": "نامعتبر", "profiles.editInfo.intervalMinutes": "فاصله زمانی ثابت به دقیقه", diff --git a/src/renderer/src/locales/ru-RU.json b/src/renderer/src/locales/ru-RU.json index ffc001c..75da680 100644 --- a/src/renderer/src/locales/ru-RU.json +++ b/src/renderer/src/locales/ru-RU.json @@ -454,6 +454,8 @@ "profiles.editInfo.override.global": "Глобальный", "profiles.editInfo.override.noAvailable": "Нет доступных переопределений", "profiles.editInfo.override.add": "Добавить переопределение", + "profiles.editInfo.updateTimeout": "Таймаут обновления", + "profiles.editInfo.updateTimeoutPlaceholder": "Время ожидания в секундах", "profiles.editInfo.intervalPlaceholder": "например: 30 или '0 * * * *'", "profiles.editInfo.intervalInvalid": "Недействительно", "profiles.editInfo.intervalMinutes": "Фиксированный интервал в минутах", diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json index 6ba2b2d..d26077d 100644 --- a/src/renderer/src/locales/zh-CN.json +++ b/src/renderer/src/locales/zh-CN.json @@ -489,6 +489,8 @@ "profiles.editInfo.override.global": "全局", "profiles.editInfo.override.noAvailable": "没有可用的覆写", "profiles.editInfo.override.add": "添加覆写", + "profiles.editInfo.updateTimeout": "更新超时时间", + "profiles.editInfo.updateTimeoutPlaceholder": "以秒为单位的超时时间", "profiles.editFile.title": "编辑订阅", "profiles.editFile.notice": "注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用", "profiles.editFile.override": "覆写", diff --git a/src/renderer/src/locales/zh-TW.json b/src/renderer/src/locales/zh-TW.json index f493950..62c1fe3 100644 --- a/src/renderer/src/locales/zh-TW.json +++ b/src/renderer/src/locales/zh-TW.json @@ -489,6 +489,8 @@ "profiles.editInfo.override.global": "全局", "profiles.editInfo.override.noAvailable": "沒有可用的覆寫", "profiles.editInfo.override.add": "添加覆寫", + "profiles.editInfo.updateTimeout": "更新逾時時間", + "profiles.editInfo.updateTimeoutPlaceholder": "以秒為單位的超時時間", "profiles.editFile.title": "編輯訂閱", "profiles.editFile.notice": "注意:此處編輯配置更新訂閱後會還原,如需要自定義配置請使用", "profiles.editFile.override": "覆寫", diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 0eadc19..da520f0 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -497,6 +497,7 @@ interface IProfileItem { allowFixedInterval?: boolean autoUpdate?: boolean authToken?: string + updateTimeout?: number } interface ISubStoreSub {