// ==UserScript==// @name         IP-only Geolocation (Enhanced)// @namespace    http://tampermonkey.net/// @version      0.2// @description  让 geolocation 定位随 IP 变化;若 IP 接口失败/超时则拒绝定位请求// @author       juan// @match        *://*/*// @grant        GM_xmlhttpRequest// @run-at       document-start// @connect      ipapi.co// @connect      ip-api.com// @license      CC-BY-NC-SA-4.0// ==/UserScript==/**- IP-only Geolocation Userscript- - 版权所有 (c) 2024- - 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可- This work is licensed under CC BY-NC-SA 4.0- https://creativecommons.org/licenses/by-nc-sa/4.0/- - 您可以自由地:- - 共享 — 在任何媒介以任何形式复制、发行本作品- - 演绎 — 修改、转换或以本作品为基础进行创作- - 惟须遵守下列条件:- - 署名 — 您必须给出适当的署名- - 非商业性使用 — 您不得将本作品用于商业目的- - 相同方式共享 — 如果您再混合、转换或者基于本作品进行创作,- 您必须基于与原先许可协议相同的许可协议分发您贡献的作品  */(function () {‘use strict’;// ========== 配置 ==========const CONFIG = {    // 主 API(无需 key)    PRIMARY_API: 'https://ipapi.co/json/',    // 备用 API    FALLBACK_API: 'https://ip-api.com/json/',    // 请求超时(毫秒)    TIMEOUT: 3000,    // IP 定位结果缓存时间(毫秒),默认 5 分钟    CACHE_DURATION: 5 * 60 * 1000,    // 是否启用控制台调试日志    DEBUG: false};// ========== 状态管理 ==========let ipLocation = null;let ipFetchInProgress = false;let ipFetchFailed = false;let cacheTimestamp = 0;const pendingCallbacks = [];const activeWatchers = new Map(); // watchId -> intervalId// 调试日志function debug(...args) {    if (CONFIG.DEBUG) {        console.log('[IP-Geo]', ...args);    }}// 浏览器不支持 geolocation 的情况if (!('geolocation' in navigator)) {    debug('Geolocation not supported, script disabled');    return;}// ========== API 解析器 ==========const API_PARSERS = {    // ipapi.co 格式    'ipapi.co': (data) => {        if (typeof data.latitude === 'number' && typeof data.longitude === 'number') {            return {                lat: data.latitude,                lon: data.longitude,                city: data.city || '',                region: data.region || '',                country: data.country_name || data.country || '',                accuracy: 50000            };        }        return null;    },    // ip-api.com 格式    'ip-api.com': (data) => {        if (data.status === 'success' && typeof data.lat === 'number' && typeof data.lon === 'number') {            return {                lat: data.lat,                lon: data.lon,                city: data.city || '',                region: data.regionName || '',                country: data.country || '',                accuracy: 30000            };        }        return null;    }};// 检测 API URL 对应的解析器function getParser(url) {    for (const [key, parser] of Object.entries(API_PARSERS)) {        if (url.includes(key)) return parser;    }    return API_PARSERS['ipapi.co']; // 默认}// ========== 位置对象构造 ==========function makePositionFromIp(loc) {    return {        coords: {            latitude: loc.lat,            longitude: loc.lon,            accuracy: loc.accuracy || 50000,            altitude: null,            altitudeAccuracy: null,            heading: null,            speed: null        },        timestamp: Date.now()    };}function makeRejectError(code, message) {    return {        code: code, // 1=PERMISSION_DENIED, 2=POSITION_UNAVAILABLE, 3=TIMEOUT        message: message,        PERMISSION_DENIED: 1,        POSITION_UNAVAILABLE: 2,        TIMEOUT: 3    };}// ========== IP 定位请求 ==========function requestIpApi(apiUrl, onSuccess, onFailure) {    debug('Requesting:', apiUrl);        GM_xmlhttpRequest({        method: 'GET',        url: apiUrl,        timeout: CONFIG.TIMEOUT,        onload: function (res) {            try {                const data = JSON.parse(res.responseText || '{}');                const parser = getParser(apiUrl);                const loc = parser(data);                                if (loc) {                    debug('IP location obtained:', loc);                    onSuccess(loc);                } else {                    debug('Invalid data from API');                    onFailure();                }            } catch (e) {                debug('Parse error:', e);                onFailure();            }        },        onerror: function (err) {            debug('Request error:', err);            onFailure();        },        ontimeout: function () {            debug('Request timeout');            onFailure();        }    });}// 带备用 API 的 IP 定位function fetchIpLocationOnce() {    if (ipFetchInProgress) return;        // 检查缓存    if (ipLocation && (Date.now() - cacheTimestamp) < CONFIG.CACHE_DURATION) {        debug('Using cached location');        return;    }    ipFetchInProgress = true;    ipFetchFailed = false;    // 尝试主 API    requestIpApi(        CONFIG.PRIMARY_API,        (loc) => {            ipLocation = loc;            cacheTimestamp = Date.now();            ipFetchInProgress = false;            notifyPendingCallbacks(true);        },        () => {            debug('Primary API failed, trying fallback...');            // 主 API 失败,尝试备用            requestIpApi(                CONFIG.FALLBACK_API,                (loc) => {                    ipLocation = loc;                    cacheTimestamp = Date.now();                    ipFetchInProgress = false;                    notifyPendingCallbacks(true);                },                () => {                    debug('All APIs failed');                    ipFetchFailed = true;                    ipFetchInProgress = false;                    notifyPendingCallbacks(false);                }            );        }    );}function notifyPendingCallbacks(success) {    while (pendingCallbacks.length) {        const cb = pendingCallbacks.shift();        try {            cb(success);        } catch (e) {            debug('Callback error:', e);        }    }}function ensureIpLocation(callback) {    // 检查缓存有效性    if (ipLocation && (Date.now() - cacheTimestamp) < CONFIG.CACHE_DURATION) {        callback(true);        return;    }    // 缓存过期,清除旧数据    if (ipLocation && (Date.now() - cacheTimestamp) >= CONFIG.CACHE_DURATION) {        debug('Cache expired, refetching...');        ipLocation = null;        ipFetchFailed = false;    }    if (ipFetchFailed) {        callback(false);        return;    }    pendingCallbacks.push(callback);    fetchIpLocationOnce();}// ========== Geolocation API 实现 ==========function ipOnlyGetCurrentPosition(success, error, options) {    if (typeof success !== 'function') {        debug('Invalid success callback');        return;    }    const timeout = options?.timeout || 10000;    let timedOut = false;    // 设置超时    const timeoutId = setTimeout(() => {        timedOut = true;        if (typeof error === 'function') {            error(makeRejectError(3, 'Location request timed out'));        }    }, timeout);    ensureIpLocation((ok) => {        if (timedOut) return;        clearTimeout(timeoutId);        if (ok && ipLocation) {            try {                success(makePositionFromIp(ipLocation));            } catch (e) {                debug('Success callback error:', e);            }        } else {            if (typeof error === 'function') {                try {                    error(makeRejectError(2, 'IP geolocation unavailable'));                } catch (e) {                    debug('Error callback error:', e);                }            }        }    });}function ipOnlyWatchPosition(success, error, options) {    const watchId = Date.now() + Math.random();        // 立即获取一次位置    ipOnlyGetCurrentPosition(success, error, options);    // 设置定期更新(如果缓存过期会自动重新请求)    const updateInterval = options?.maximumAge || CONFIG.CACHE_DURATION;    const intervalId = setInterval(() => {        ipOnlyGetCurrentPosition(success, error, options);    }, updateInterval);    activeWatchers.set(watchId, intervalId);    debug('Watch started:', watchId);    return watchId;}function ipOnlyClearWatch(watchId) {    if (activeWatchers.has(watchId)) {        clearInterval(activeWatchers.get(watchId));        activeWatchers.delete(watchId);        debug('Watch cleared:', watchId);    }}// ========== 覆写 Navigator.geolocation ==========try {    const originalGeo = navigator.geolocation;        // 使用 Object.defineProperty 强制覆写    Object.defineProperty(navigator, 'geolocation', {        value: {            getCurrentPosition: ipOnlyGetCurrentPosition,            watchPosition: ipOnlyWatchPosition,            clearWatch: ipOnlyClearWatch        },        writable: false,        configurable: true    });    debug('Geolocation API successfully overridden');} catch (e) {    // 降级方案:直接修改方法    try {        const geo = navigator.geolocation;        geo.getCurrentPosition = ipOnlyGetCurrentPosition;        geo.watchPosition = ipOnlyWatchPosition;        geo.clearWatch = ipOnlyClearWatch;        debug('Geolocation methods overridden (fallback mode)');    } catch (e2) {        console.warn('[IP-Geo] Failed to override geolocation:', e2);    }}// ========== 页面卸载时清理 ==========window.addEventListener('beforeunload', () => {    activeWatchers.forEach((intervalId) => clearInterval(intervalId));    activeWatchers.clear();});debug('Script initialized');})();