import { getName, getVersion } from '@tauri-apps/api/app' import { fetch } from '@tauri-apps/plugin-http' import { asyncRetry } from 'foxts/async-retry' import { extractErrorMessage } from 'foxts/extract-error-message' import { once } from 'foxts/once' import i18n from 'i18next' import { debugLog } from '@/utils/debug' const getUserAgentPromise = once(async () => { try { const [name, version] = await Promise.all([getName(), getVersion()]) return `${name}/${version}` } catch (error) { console.debug('Failed to build User-Agent, fallback to default', error) return 'clash-verge-rev' } }) // Get current IP and geolocation information (refactored IP detection with service-specific mappings) interface IpInfo { ip: string country_code: string country: string region: string city: string organization: string asn: number asn_organization: string longitude: number latitude: number timezone: string } // IP检测服务配置 interface ServiceConfig { url: string mapping: (data: any) => IpInfo timeout?: number // 保留timeout字段(如有需要) } // 可用的IP检测服务列表及字段映射 const IP_CHECK_SERVICES: ServiceConfig[] = [ { url: 'https://api.ip.sb/geoip', mapping: (data) => ({ ip: data.ip || '', country_code: data.country_code || '', country: data.country || '', region: data.region || '', city: data.city || '', organization: data.organization || data.isp || '', asn: data.asn || 0, asn_organization: data.asn_organization || '', longitude: data.longitude || 0, latitude: data.latitude || 0, timezone: data.timezone || '', }), }, { url: 'https://ipapi.co/json', mapping: (data) => ({ ip: data.ip || '', country_code: data.country_code || '', country: data.country_name || '', region: data.region || '', city: data.city || '', organization: data.org || '', asn: data.asn ? parseInt(data.asn.replace('AS', '')) : 0, asn_organization: data.org || '', longitude: data.longitude || 0, latitude: data.latitude || 0, timezone: data.timezone || '', }), }, { url: 'https://api.ipapi.is/', mapping: (data) => ({ ip: data.ip || '', country_code: data.location?.country_code || '', country: data.location?.country || '', region: data.location?.state || '', city: data.location?.city || '', organization: data.asn?.org || data.company?.name || '', asn: data.asn?.asn || 0, asn_organization: data.asn?.org || '', longitude: data.location?.longitude || 0, latitude: data.location?.latitude || 0, timezone: data.location?.timezone || '', }), }, { url: 'https://ipwho.is/', mapping: (data) => ({ ip: data.ip || '', country_code: data.country_code || '', country: data.country || '', region: data.region || '', city: data.city || '', organization: data.connection?.org || data.connection?.isp || '', asn: data.connection?.asn || 0, asn_organization: data.connection?.isp || '', longitude: data.longitude || 0, latitude: data.latitude || 0, timezone: data.timezone?.id || '', }), }, { url: 'https://ip.api.skk.moe/cf-geoip', mapping: (data) => ({ ip: data.ip || '', country_code: data.country || '', country: data.country || '', region: data.region || '', city: data.city || '', organization: data.asOrg || '', asn: data.asn || 0, asn_organization: data.asOrg || '', longitude: data.longitude || 0, latitude: data.latitude || 0, timezone: data.timezone || '', }), }, { url: 'https://get.geojs.io/v1/ip/geo.json', mapping: (data) => ({ ip: data.ip || '', country_code: data.country_code || '', country: data.country || '', region: data.region || '', city: data.city || '', organization: data.organization_name || '', asn: data.asn || 0, asn_organization: data.organization_name || '', longitude: Number(data.longitude) || 0, latitude: Number(data.latitude) || 0, timezone: data.timezone || '', }), }, ] // 获取当前IP和地理位置信息 export const getIpInfo = async (): Promise< IpInfo & { lastFetchTs: number } > => { // 配置参数 const maxRetries = 2 const serviceTimeout = 5000 const shuffledServices = IP_CHECK_SERVICES.slice().sort( () => Math.random() - 0.5, ) let lastError: unknown | null = null const userAgent = await getUserAgentPromise() console.debug('User-Agent for IP detection:', userAgent) for (const service of shuffledServices) { debugLog(`尝试IP检测服务: ${service.url}`) const timeoutController = new AbortController() const timeoutId = setTimeout(() => { timeoutController.abort() }, service.timeout || serviceTimeout) try { return await asyncRetry( async (bail) => { console.debug('Fetching IP information:', service.url) const response = await fetch(service.url, { method: 'GET', signal: timeoutController.signal, // AbortSignal.timeout(service.timeout || serviceTimeout), connectTimeout: service.timeout || serviceTimeout, headers: { 'User-Agent': userAgent, }, }) if (!response.ok) { return bail( new Error( `IP 检测服务出错,状态码: ${response.status} from ${service.url}`, ), ) } let data: any try { data = await response.json() } catch { return bail(new Error(`无法解析 JSON 响应 from ${service.url}`)) } if (data && data.ip) { debugLog(`IP检测成功,使用服务: ${service.url}`) return Object.assign(service.mapping(data), { // use last fetch success timestamp lastFetchTs: Date.now(), }) } else { return bail(new Error(`无效的响应格式 from ${service.url}`)) } }, { retries: maxRetries, minTimeout: 1000, maxTimeout: 4000, randomize: true, }, ) } catch (error) { debugLog(`IP检测服务失败: ${service.url}`, error) lastError = error } finally { clearTimeout(timeoutId) } } if (lastError) { console.error('[IP Info] All detection services failed:', lastError) throw new Error( i18n.t(($) => $.home.components.ipInfo.errors.loadWithDetails, { message: extractErrorMessage(lastError) || i18n.t(($) => $.shared.feedback.errors.unknown), }), ) } else { throw new Error(i18n.t(($) => $.home.components.ipInfo.errors.noServices)) } }