diff --git a/.env.development b/.env.development index 84d3474..38a94a6 100644 --- a/.env.development +++ b/.env.development @@ -6,3 +6,4 @@ VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 0 # 是否开启参数加密 0关闭 1开启 # VITE_BASE_URL= ["http://172.104.39.32:25508"] # api选线域名,这里必须为数组 VITE_BASE_URL= ["https://d1s84171319hi1.cloudfront.net","https://d2j97wqe01j5pa.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = hgdj.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 diff --git a/.env.production b/.env.production index 27315b6..66308b4 100644 --- a/.env.production +++ b/.env.production @@ -5,3 +5,4 @@ VITE_BUILD_DROP_CONSOLE = true # 是否在打包时删除 console 代码 VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 1 # 是否开启参数加密 0关闭 1开启 VITE_BASE_URL= ["https://d1s84171319hi1.cloudfront.net","https://d2j97wqe01j5pa.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = hgdj.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 diff --git a/.env.test b/.env.test index 0a9c43c..bc0e4c0 100644 --- a/.env.test +++ b/.env.test @@ -5,3 +5,4 @@ VITE_BUILD_DROP_CONSOLE = true # 是否在打包时删除 console 代码 VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 0 # 是否开启参数加密 0关闭 1开启 VITE_BASE_URL= ["https://d1s84171319hi1.cloudfront.net","https://d2j97wqe01j5pa.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = hgdj.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..7b1090e Binary files /dev/null and b/dist.zip differ diff --git a/src/utils/detectLine.ts b/src/utils/detectLine.ts new file mode 100644 index 0000000..7f713f3 --- /dev/null +++ b/src/utils/detectLine.ts @@ -0,0 +1,121 @@ +import axios from 'axios'; + +/** + * 选线:本地缓存线路 → 内置兜底线路 → DNS 解析,同安卓 DetectLineManager。 + * + * 内置线路全挂时,去解析 VITE_DNS_DOMAIN 的 TXT 记录拿线路(运维提供,多条用 _ 分隔), + * 解析出来的线路存本地,下次启动优先用它们。 + */ + +/** DNS 解析出来的线路缓存 key(同安卓 StoreKeys.DETECT_LINE) */ +const LINE_CACHE_KEY = 'detect_line'; + +/** 单次 ping / DNS 请求超时 */ +const TIMEOUT = 1000 * 10; + +/** DoH 服务商,按顺序试:阿里(安卓用的也是这家)→ Cloudflare 兜底 */ +const DNS_PROVIDERS = ['https://dns.alidns.com/resolve', 'https://cloudflare-dns.com/dns-query']; + +/** 内置兜底线路(.env 的 VITE_BASE_URL) */ +const builtinLines = (): string[] => { + try { + const lines = JSON.parse(process.env.VITE_BASE_URL || '[]'); + return Array.isArray(lines) ? lines.filter(Boolean) : []; + } catch { + return []; + } +}; + +/** 上次 DNS 解析出来的线路 */ +const cachedLines = (): string[] => { + try { + const lines = JSON.parse(localStorage.getItem(LINE_CACHE_KEY) || '[]'); + return Array.isArray(lines) ? lines.filter(Boolean) : []; + } catch { + return []; + } +}; + +const saveCachedLines = (lines: string[]): void => { + try { + localStorage.setItem(LINE_CACHE_KEY, JSON.stringify(lines)); + } catch { + // 无痕模式等写不进去,不影响本次选线 + } +}; + +/** ping 单条线路,不抛错:通了返回 true */ +const pingLine = async (line: string): Promise => { + try { + const { data } = await axios.get(line + '/api/app/ping/check', { timeout: TIMEOUT }); + return data?.code == 200; + } catch { + return false; + } +}; + +/** 并发 ping 一批线路:谁先成功用谁;全失败返回 '' */ +const pingBatch = (lines: string[]): Promise => + new Promise((resolve) => { + if (!lines.length) return resolve(''); + let failCount = 0; + let settled = false; + lines.forEach((line) => { + void pingLine(line).then((ok) => { + if (settled) return; + if (ok) { + settled = true; + resolve(line); + return; + } + if (++failCount >= lines.length) { + settled = true; + resolve(''); + } + }); + }); + }); + +/** + * 解析 DNS TXT 记录拿线路。 + * TXT 值形如 "https://a.com_https://b.com",带引号,按 _ 拆成多条 + */ +const dnsSolve = async (): Promise => { + const domain = process.env.VITE_DNS_DOMAIN; + if (!domain) return []; + for (const provider of DNS_PROVIDERS) { + try { + const { data } = await axios.get(provider, { + params: { name: domain, type: 'TXT' }, + headers: { accept: 'application/dns-json' }, + timeout: TIMEOUT, + }); + const lines: string[] = (data?.Answer || []) + .flatMap((record: any) => String(record?.data || '').split('_')) + .map((line: string) => line.replace(/"/g, '').trim()) + .filter(Boolean); + if (lines.length) return lines; + } catch { + // 这家解析不出来就换下一家 + } + } + return []; +}; + +/** + * 完整选线:缓存线路(没有就用内置线路)全挂时回退 DNS 解析。 + * 返回通了的线路,全都不通返回 '' + */ +export const detectLine = async (): Promise => { + const cached = cachedLines(); + const line = await pingBatch(cached.length ? cached : builtinLines()); + if (line) return line; + + // 本地线路全部失败,回退 DNS 解析 + const dnsLines = await dnsSolve(); + if (!dnsLines.length) return ''; + saveCachedLines(dnsLines); + return await pingBatch(dnsLines); +}; + +export default detectLine; diff --git a/src/utils/ping.ts b/src/utils/ping.ts index 23185cf..1b61cd5 100644 --- a/src/utils/ping.ts +++ b/src/utils/ping.ts @@ -1,57 +1,13 @@ import { useConfigStore } from './../stores/config'; -import axios from 'axios'; +import { detectLine } from './detectLine'; /** - * 测试域名 - * @param url - * @returns - */ -async function testPing(url: string): Promise { - try { - const ret = await axios({ - method: 'GET', - url: url + '/api/app/ping/check', - timeout: 1000 * 10, // 10秒超时 - }); - - if (ret.data.code == 200) { - return true; - } - return false; - } catch (error) { - return false; - } -} - -/** - * 将数组乱序 - * @param arr - * @returns - */ -function shuffle(arr: Array): Array { - let m = arr.length; - while (m > 1) { - const index = Math.floor(Math.random() * m--); - [arr[m], arr[index]] = [arr[index], arr[m]]; - } - return arr; -} - -/** - * 选线 - * @returns + * 选线:本地线路 → 全挂则 DNS 解析兜底(实现见 utils/detectLine) + * @returns 是否选到可用线路 */ export default async function pingCheck(): Promise { - const baseUrls: string = process.env.VITE_BASE_URL || '[]'; - const hosts = JSON.parse(baseUrls); - const allLine = shuffle(hosts); - for (const line of allLine) { - const isOk = await testPing(line); - if (isOk) { - //选线成功 - useConfigStore().saveBaseUrl(line); - return true; - } - } - return false; + const line = await detectLine(); + if (!line) return false; + useConfigStore().saveBaseUrl(line); + return true; } diff --git a/src/views/Splash.vue b/src/views/Splash.vue index b483d62..9e08690 100644 --- a/src/views/Splash.vue +++ b/src/views/Splash.vue @@ -33,9 +33,9 @@ import { reactive } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { jumpAdv } from '@/utils/index'; import { useGlobalStore } from '@/stores/global'; -import axios from 'axios'; import setTheme from '@/assets/theme/index'; import { markHomeOldUserGuidePendingFromSplash } from '@/utils/paymentGuide'; +import { detectLine } from '@/utils/detectLine'; const route = useRoute(); const router = useRouter(); @@ -50,7 +50,6 @@ const state = reactive({ checkTimer: null, //检测图片是否加载成功定时器 checkTime: 3, //图片加载成功检测时间 isResLock: false, // 请求结果锁 - lineFailCount: 0, // 已失败的线路数 loading: true, // 选线loading isUpgrade: true, //是否升级 }); @@ -349,16 +348,6 @@ const getConfig = async (): Promise => { return false; } }; -/** - * @description 选线失败:同安卓,失败不做处理(不弹重试/线路异常弹窗)。 - * 全部线路都不通就收起「选线中...」,启动流程停在启动页 - */ -const onLineCheckFail = (hosts: any) => { - state.lineFailCount++; - if (state.lineFailCount >= hosts.length && !state.isResLock) { - state.loading = false; - } -}; /** * 初始化 */ @@ -367,30 +356,18 @@ const init = async () => { route.query.isAppStore ? localStorage.setItem('isAppStore', route.query.isAppStore.toString()) : localStorage.setItem('isAppStore', ''); // 保存是否来自商店包,用于判断是否跳转外链 - const baseUrls: string = process.env.VITE_BASE_URL || '[]'; - const hosts = JSON.parse(baseUrls); - hosts.forEach((url: any) => { - axios({ - method: 'GET', - url: url + '/api/app/ping/check', - timeout: 1000 * 10, // 10秒超时 - }) - .then(async (res) => { - if (res.data.code == 200) { - if (!state.isResLock) { - state.isResLock = true; - //选线成功 - useConfigStore().saveBaseUrl(url); - await ensureAuthAndConfig(); - } - } else { - onLineCheckFail(hosts); - } - }) - .catch(() => { - onLineCheckFail(hosts); - }); - }); + // 选线:本地线路 → 全挂则 DNS 解析兜底,同安卓 DetectLineManager + const line = await detectLine(); + // 选线失败:同安卓,失败不做处理(不弹重试/线路异常弹窗), + // 收起「选线中...」,启动流程停在启动页 + if (!line) { + state.loading = false; + return; + } + if (state.isResLock) return; + state.isResLock = true; + configStore.saveBaseUrl(line); + await ensureAuthAndConfig(); }; /** diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index ce8d1eb..90d4102 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,6 +5,7 @@ interface ImportMetaEnv { readonly VITE_BUILD_SOURCEMAP: string; readonly VITE_BUILD_DROP_CONSOLE: string; readonly VITE_BASE_URL: string; + readonly VITE_DNS_DOMAIN: string; readonly VITE_PARAMETER_ENCRYPTION: string; }