Files
clash-verge-rev/src/services/api.ts
T
Tunglies e56711b2db refactor(frontend): drop unused params, props and api surface
Remove the never-read _revalidate argument of mutateVerge and its 16
call-site placeholders, seven never-passed props of the split chip
editor, the unconsumed refreshDecorated context member, the now-orphaned
toggleFullscreen control, the unused translate argument of the notice
message handler, the reserved-but-unset ServiceConfig.timeout field and
unreachable branches in parseNoticeExtras.
2026-10-28 06:30:46 +08:00

230 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
// 可用的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()
}, 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(serviceTimeout),
connectTimeout: 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))
}
}