mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-25 05:02:07 +08:00
231 lines
6.8 KiB
TypeScript
231 lines
6.8 KiB
TypeScript
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))
|
||
}
|
||
}
|