mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 10:42:09 +08:00
225 lines
6.6 KiB
TypeScript
225 lines
6.6 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 { 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.toSorted(
|
||
() => 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) {
|
||
throw new Error(
|
||
`所有IP检测服务都失败: ${extractErrorMessage(lastError) || "未知错误"}`,
|
||
);
|
||
} else {
|
||
throw new Error("没有可用的IP检测服务");
|
||
}
|
||
};
|