From 55416f32cdacbe3a7aa905867d401cc7ba5a1ddb Mon Sep 17 00:00:00 2001 From: xmk23333 Date: Tue, 16 Dec 2025 13:22:26 +0800 Subject: [PATCH] feat: Automatically Choose Direct or Proxy during Subscription Import #issue 1450 --- .gitignore | 3 +- src/main/config/profile.ts | 154 ++++++++++++++++++++++--------------- 2 files changed, 96 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 94ebee6..6789f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ out .idea *.ttf party.md -CLAUDE.md \ No newline at end of file +CLAUDE.md +tsconfig.node.tsbuildinfo diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index 475e835..88dfb4e 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -129,6 +129,60 @@ export async function getCurrentProfileItem(): Promise { return (await getProfileItem(current)) || { id: 'default', type: 'local', name: '空白订阅' } } +interface FetchOptions { + url: string + useProxy: boolean + mixedPort: number + userAgent: string + authToken?: string + timeout: number + substore: boolean +} + +interface FetchResult { + data: string + headers: Record +} + +async function fetchAndValidateSubscription(options: FetchOptions): Promise { + const { url, useProxy, mixedPort, userAgent, authToken, timeout, substore } = options + + const headers: Record = { 'User-Agent': userAgent } + if (authToken) headers['Authorization'] = authToken + + let res: chromeRequest.Response + if (substore) { + const urlObj = new URL(`http://127.0.0.1:${subStorePort}${url}`) + urlObj.searchParams.set('target', 'ClashMeta') + urlObj.searchParams.set('noCache', 'true') + if (useProxy) { + urlObj.searchParams.set('proxy', `http://127.0.0.1:${mixedPort}`) + } + res = await chromeRequest.get(urlObj.toString(), { headers, responseType: 'text', timeout }) + } else { + res = await chromeRequest.get(url, { + headers, + responseType: 'text', + timeout, + proxy: useProxy ? { protocol: 'http', host: '127.0.0.1', port: mixedPort } : false + }) + } + + if (res.status < 200 || res.status >= 300) { + throw new Error(`Subscription failed: Request status code ${res.status}`) + } + + const parsed = parse(res.data) as Record | null + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('Subscription failed: Profile is not a valid YAML') + } + if (!parsed['proxies'] && !parsed['proxy-providers']) { + throw new Error('Subscription failed: Profile missing proxies or providers') + } + + return { data: res.data, headers: res.headers } +} + export async function createProfile(item: Partial): Promise { const id = item.id || new Date().getTime().toString(16) const newItem = { @@ -145,71 +199,54 @@ export async function createProfile(item: Partial): Promise - if (newItem.substore) { - const urlObj = new URL(`http://127.0.0.1:${subStorePort}${item.url}`) - urlObj.searchParams.set('target', 'ClashMeta') - urlObj.searchParams.set('noCache', 'true') - if (newItem.useProxy) { - urlObj.searchParams.set('proxy', `http://127.0.0.1:${mixedPort}`) - } else { - urlObj.searchParams.delete('proxy') - } - const headers: Record = { - 'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)` - } - if (item.authToken) { - headers['Authorization'] = item.authToken - } - res = await chromeRequest.get(urlObj.toString(), { - headers, - responseType: 'text', + + const baseOptions: Omit = { + url: item.url, + mixedPort, + userAgent: userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`, + authToken: item.authToken, + substore: newItem.substore || false + } + + let result: FetchResult + let finalUseProxy = newItem.useProxy + + if (newItem.useProxy) { + result = await fetchAndValidateSubscription({ + ...baseOptions, + useProxy: true, timeout: subscriptionTimeout }) } else { - const headers: Record = { - 'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)` + 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 + } } - if (item.authToken) { - headers['Authorization'] = item.authToken - } - res = await chromeRequest.get(item.url, { - proxy: newItem.useProxy - ? { - protocol: 'http', - host: '127.0.0.1', - port: mixedPort - } - : false, - headers, - responseType: 'text', - timeout: subscriptionTimeout - }) } - // 检查状态码,例如:403 - if (res.status < 200 || res.status >= 300) { - throw new Error(`Subscription failed: Request status code ${res.status}`) - } - - const data = res.data - const headers = res.headers - - // 校验是否为对象结构 (拦截 HTML字符串、普通文本、乱码) - const parsed = parse(data) - if (typeof parsed !== 'object' || parsed === null) { - throw new Error('Subscription failed: Profile is not a valid YAML') - } - // 检查是否包含必要的字段,防止空对象 - const profile = parsed as any - if (!profile.proxies && !profile['proxy-providers']) { - throw new Error('Subscription failed: Profile missing proxies or providers') - } + newItem.useProxy = finalUseProxy + const { data, headers } = result if (headers['content-disposition'] && newItem.name === 'Remote File') { newItem.name = parseFilename(headers['content-disposition']) @@ -217,10 +254,8 @@ export async function createProfile(item: Partial): Promise): Promise