Files
clash-verge-rev/src/services/api.ts
T
2026-02-12 08:49:41 +08:00

225 lines
6.6 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 { 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检测服务");
}
};