// ==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');})();
评论 (0)