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
This commit is contained in:
julong 2026-01-23 20:42:45 +08:00 committed by GitHub
parent 767cdfeef3
commit d3a23a0601
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 87 additions and 73 deletions

View File

@ -218,10 +218,10 @@ async function fetchAndValidateSubscription(options: FetchOptions): Promise<Fetc
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> { export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
const id = item.id || new Date().getTime().toString(16) const id = item.id || new Date().getTime().toString(16)
const newItem = { const newItem: IProfileItem = {
id, id,
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'), name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
type: item.type, type: item.type!,
url: item.url, url: item.url,
substore: item.substore || false, substore: item.substore || false,
interval: item.interval || 0, interval: item.interval || 0,
@ -230,85 +230,66 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
allowFixedInterval: item.allowFixedInterval || false, allowFixedInterval: item.allowFixedInterval || false,
autoUpdate: item.autoUpdate ?? false, autoUpdate: item.autoUpdate ?? false,
authToken: item.authToken, authToken: item.authToken,
updated: new Date().getTime() updated: new Date().getTime(),
} as IProfileItem updateTimeout: item.updateTimeout || 5
}
switch (newItem.type) { // Local
case 'remote': { if (newItem.type === 'local') {
const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig() await setProfileStr(id, item.file || '')
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig() return newItem
if (!item.url) throw new Error('Empty URL') }
const baseOptions: Omit<FetchOptions, 'useProxy' | 'timeout'> = { // Remote
url: item.url, if (!item.url) throw new Error('Empty URL')
mixedPort,
userAgent: userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`,
authToken: item.authToken,
substore: newItem.substore || false
}
let result: FetchResult const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig()
let finalUseProxy = newItem.useProxy const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const userItemTimeoutMs = (newItem.updateTimeout || 5) * 1000
if (newItem.useProxy) { const baseOptions: Omit<FetchOptions, 'useProxy' | 'timeout'> = {
result = await fetchAndValidateSubscription({ url: item.url,
...baseOptions, mixedPort,
useProxy: true, userAgent: userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`,
timeout: subscriptionTimeout authToken: item.authToken,
}) substore: newItem.substore || false
} 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
}
}
}
newItem.useProxy = finalUseProxy const fetchSub = (useProxy: boolean, timeout: number) =>
const { data, headers } = result fetchAndValidateSubscription({ ...baseOptions, useProxy, timeout })
if (headers['content-disposition'] && newItem.name === 'Remote File') { let result: FetchResult
newItem.name = parseFilename(headers['content-disposition']) 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 return newItem
} }

View File

@ -32,7 +32,10 @@ const EditInfoModal: React.FC<Props> = (props) => {
const { item, updateProfileItem, onClose } = props const { item, updateProfileItem, onClose } = props
const { overrideConfig } = useOverrideConfig() const { overrideConfig } = useOverrideConfig()
const { items: overrideItems = [] } = overrideConfig || {} 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 inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]'
const { t } = useTranslation() const { t } = useTranslation()
@ -40,6 +43,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
try { try {
const updatedItem = { const updatedItem = {
...values, ...values,
updateTimeout: values.updateTimeout ?? 5,
override: values.override?.filter( override: values.override?.filter(
(i) => (i) =>
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
@ -192,6 +196,24 @@ const EditInfoModal: React.FC<Props> = (props) => {
)} )}
</> </>
)} )}
<SettingItem title={t('profiles.editInfo.updateTimeout')}>
<Input
size="sm"
type="text"
className={cn(inputWidth)}
value={values.updateTimeout?.toString() ?? ''}
onValueChange={(v) => {
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')}
/>
</SettingItem>
<SettingItem title={t('profiles.editInfo.override.title')}> <SettingItem title={t('profiles.editInfo.override.title')}>
<div> <div>
{overrideItems {overrideItems

View File

@ -484,6 +484,8 @@
"profiles.editInfo.override.global": "Global", "profiles.editInfo.override.global": "Global",
"profiles.editInfo.override.noAvailable": "No available overrides", "profiles.editInfo.override.noAvailable": "No available overrides",
"profiles.editInfo.override.add": "Add Override", "profiles.editInfo.override.add": "Add Override",
"profiles.editInfo.updateTimeout": "Update Timeout",
"profiles.editInfo.updateTimeoutPlaceholder": "Timeout in seconds",
"profiles.editFile.title": "Edit Profile", "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.notice": "Note: Changes made here will be reset after profile update. For custom configurations, please use",
"profiles.editFile.override": "Override", "profiles.editFile.override": "Override",

View File

@ -454,6 +454,8 @@
"profiles.editInfo.override.global": "جهانی", "profiles.editInfo.override.global": "جهانی",
"profiles.editInfo.override.noAvailable": "جایگزینی‌ای در دسترس نیست", "profiles.editInfo.override.noAvailable": "جایگزینی‌ای در دسترس نیست",
"profiles.editInfo.override.add": "افزودن جایگزینی", "profiles.editInfo.override.add": "افزودن جایگزینی",
"profiles.editInfo.updateTimeout": "زمان انتظار به‌روزرسانی",
"profiles.editInfo.updateTimeoutPlaceholder": "زمان پایان دادن به ثانیه",
"profiles.editInfo.intervalPlaceholder": "مثال: 30 یا '0 * * * *'", "profiles.editInfo.intervalPlaceholder": "مثال: 30 یا '0 * * * *'",
"profiles.editInfo.intervalInvalid": "نامعتبر", "profiles.editInfo.intervalInvalid": "نامعتبر",
"profiles.editInfo.intervalMinutes": "فاصله زمانی ثابت به دقیقه", "profiles.editInfo.intervalMinutes": "فاصله زمانی ثابت به دقیقه",

View File

@ -454,6 +454,8 @@
"profiles.editInfo.override.global": "Глобальный", "profiles.editInfo.override.global": "Глобальный",
"profiles.editInfo.override.noAvailable": "Нет доступных переопределений", "profiles.editInfo.override.noAvailable": "Нет доступных переопределений",
"profiles.editInfo.override.add": "Добавить переопределение", "profiles.editInfo.override.add": "Добавить переопределение",
"profiles.editInfo.updateTimeout": "Таймаут обновления",
"profiles.editInfo.updateTimeoutPlaceholder": "Время ожидания в секундах",
"profiles.editInfo.intervalPlaceholder": "например: 30 или '0 * * * *'", "profiles.editInfo.intervalPlaceholder": "например: 30 или '0 * * * *'",
"profiles.editInfo.intervalInvalid": "Недействительно", "profiles.editInfo.intervalInvalid": "Недействительно",
"profiles.editInfo.intervalMinutes": "Фиксированный интервал в минутах", "profiles.editInfo.intervalMinutes": "Фиксированный интервал в минутах",

View File

@ -489,6 +489,8 @@
"profiles.editInfo.override.global": "全局", "profiles.editInfo.override.global": "全局",
"profiles.editInfo.override.noAvailable": "没有可用的覆写", "profiles.editInfo.override.noAvailable": "没有可用的覆写",
"profiles.editInfo.override.add": "添加覆写", "profiles.editInfo.override.add": "添加覆写",
"profiles.editInfo.updateTimeout": "更新超时时间",
"profiles.editInfo.updateTimeoutPlaceholder": "以秒为单位的超时时间",
"profiles.editFile.title": "编辑订阅", "profiles.editFile.title": "编辑订阅",
"profiles.editFile.notice": "注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用", "profiles.editFile.notice": "注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用",
"profiles.editFile.override": "覆写", "profiles.editFile.override": "覆写",

View File

@ -489,6 +489,8 @@
"profiles.editInfo.override.global": "全局", "profiles.editInfo.override.global": "全局",
"profiles.editInfo.override.noAvailable": "沒有可用的覆寫", "profiles.editInfo.override.noAvailable": "沒有可用的覆寫",
"profiles.editInfo.override.add": "添加覆寫", "profiles.editInfo.override.add": "添加覆寫",
"profiles.editInfo.updateTimeout": "更新逾時時間",
"profiles.editInfo.updateTimeoutPlaceholder": "以秒為單位的超時時間",
"profiles.editFile.title": "編輯訂閱", "profiles.editFile.title": "編輯訂閱",
"profiles.editFile.notice": "注意:此處編輯配置更新訂閱後會還原,如需要自定義配置請使用", "profiles.editFile.notice": "注意:此處編輯配置更新訂閱後會還原,如需要自定義配置請使用",
"profiles.editFile.override": "覆寫", "profiles.editFile.override": "覆寫",

View File

@ -497,6 +497,7 @@ interface IProfileItem {
allowFixedInterval?: boolean allowFixedInterval?: boolean
autoUpdate?: boolean autoUpdate?: boolean
authToken?: string authToken?: string
updateTimeout?: number
} }
interface ISubStoreSub { interface ISubStoreSub {