diff --git a/.env.development b/.env.development index 6689f92..e837baf 100644 --- a/.env.development +++ b/.env.development @@ -5,6 +5,7 @@ VITE_BUILD_DROP_CONSOLE = true # 是否在打包时删除 console 代码 VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 0 # 是否开启参数加密 0关闭 1开启 # VITE_BASE_URL= ["https://91djht.remoces.com"] # api选线域名,这里必须为数组 -VITE_BASE_URL= ["https://d2huxeowqcrhxe.cloudfront.net","https://dp539615g9mv7.cloudfront.net","https://u2j5x.com"] # api选线域名,这里必须为数组 +VITE_BASE_URL= ["https://d2s4jk18c45pye.cloudfront.net","https://d1ofgr0kx88xsf.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = hgmj.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 # IM 路由域名(拉取 IM 网关配置,须与 imToken 环境一致) VITE_IM_DYNAMIC_CONFIG_DOMAIN=https://sdk.api.cdflow.cn diff --git a/.env.production b/.env.production index 87cb5fe..b91f5cf 100644 --- a/.env.production +++ b/.env.production @@ -4,6 +4,7 @@ VITE_BUILD_SOURCEMAP = false # 是否在打包时生成 sourcemap VITE_BUILD_DROP_CONSOLE = true # 是否在打包时删除 console 代码 VITE_BUILD_VCONSOLE = false # 是否开启调试工具 vconsole VITE_PARAMETER_ENCRYPTION = 1 # 是否开启参数加密 0关闭 1开启 -VITE_BASE_URL= ["https://d2huxeowqcrhxe.cloudfront.net","https://dp539615g9mv7.cloudfront.net","https://u2j5x.com"] # api选线域名,这里必须为数组 +VITE_BASE_URL= ["https://d2s4jk18c45pye.cloudfront.net","https://d1ofgr0kx88xsf.cloudfront.net"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = hgmj.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 # IM 路由域名(拉取 IM 网关配置,须与 imToken 环境一致) VITE_IM_DYNAMIC_CONFIG_DOMAIN=https://sdk.api.cdflow.cn diff --git a/.env.test b/.env.test index 14c89f0..33249d9 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://91djht.remoces.com"] # api选线域名,这里必须为数组 +VITE_DNS_DOMAIN = hgmj.mfcg.it.com # DNS 兜底域名:上面线路全挂时解析它的 TXT 记录拿线路 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 d614556..0bd10d9 100644 --- a/src/views/Splash.vue +++ b/src/views/Splash.vue @@ -39,10 +39,10 @@ import { // analyticsEventUserRegister, analyticsEventUserLogin, } from '@/utils/eventReporter/analytics'; -import axios from 'axios'; import setTheme from '@/assets/theme/index'; import { preloadImSdk, warmImIfNeeded } from '@/utils/im/sdk'; import { markHomeOldUserGuidePendingFromSplash } from '@/utils/paymentGuide'; +import { detectLine } from '@/utils/detectLine'; const route = useRoute(); const router = useRouter(); @@ -59,8 +59,6 @@ const state = reactive({ isCheckError: false, //是否选线失败 checkLineErrorNum: 0, //选线失败次数 isResLock: false, // 请求结果锁 - resResult: [], // 响应结果 - pingCheckCount: 0, // 请求的ping失败次数 loading: true, // 选线loading isUpgrade: true, //是否升级 }); @@ -374,31 +372,26 @@ const getConfig = async (): Promise => { return false; } }; -const pingErrorPop = (hosts: any) => { - // 只有最后一条域名出错时,判断响应结果不存在true的时候,说明全部不通,即弹出选线失败 - state.pingCheckCount++; - if (state.pingCheckCount == hosts.length) { - state.pingCheckCount = 0; - let index = state.resResult.indexOf(true); - if (index == -1) { - if (state.checkLineErrorNum == 1) { - state.isCheckError = true; - return; - } - showDialog({ - title: '温馨提示', - message: '选线失败', - confirmButtonText: '重试', - theme: 'round-button', - }).then(() => { - state.checkLineErrorNum++; - appStore.$reset(); - userStore.$reset(); - configStore.$reset(); - init(); - }); - } +/** + * @description 选线失败:首次弹「选线失败」可重试,重试仍失败则弹进群引导 + */ +const onLineCheckFail = () => { + if (state.checkLineErrorNum == 1) { + state.isCheckError = true; + return; } + showDialog({ + title: '温馨提示', + message: '选线失败', + confirmButtonText: '重试', + theme: 'round-button', + }).then(() => { + state.checkLineErrorNum++; + appStore.$reset(); + userStore.$reset(); + configStore.$reset(); + init(); + }); }; /** * 初始化 @@ -409,32 +402,16 @@ const init = async () => { route.query.isAppStore ? localStorage.setItem('isAppStore', route.query.isAppStore.toString()) : localStorage.setItem('isAppStore', ''); // 保存是否来自商店包,用于判断是否跳转外链 - const baseUrls: string = import.meta.env.VITE_BASE_URL || '[]'; - const hosts = JSON.parse(baseUrls); - state.resResult = new Array(hosts.length); - hosts.forEach((url: any, index: number) => { - 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; - state.resResult[index] = true; - //选线成功 - useConfigStore().saveBaseUrl(url); - await ensureAuthAndConfig(); - } - } else { - pingErrorPop(hosts); - } - }) - .catch(() => { - pingErrorPop(hosts); - }); - }); + // 选线:本地线路 → 全挂则 DNS 解析兜底,同安卓 DetectLineManager + const line = await detectLine(); + if (!line) { + onLineCheckFail(); + 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 076cfa1..6dfcd54 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; readonly VITE_IM_DYNAMIC_CONFIG_DOMAIN?: string; }