// ==UserScript== // @name 移动端去除链接重定向 // @author Deepseek@GLM // @description 通用的重定向解析 // @version 1.5 // @namespace Violentmonkey Scripts // @grant unsafeWindow // @grant GM.xmlHttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @match *://*/* // @connect * // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; // 标记属性:用于防止重复处理和点击 const REDIRECT_COMPLETED = 'data-redirect-completed'; const CLICK_HANDLED = 'data-click-handled'; // 核心配置项 const CONFIG = { excludeSameOrigin: false, // 是否排除同源链接 maxProcessTime: 200, // 最大处理时间,超时则放弃 maxResolveDepth: 2, // 最大递归解析深度 maxConcurrentRequests: 2, // 最大并发网络请求数 requestTimeout: 8000, // 网络请求超时时间 enableCache: true, // 是否开启缓存 cacheExpireDays: 7, // 缓存过期天数 maxCacheSize: 500, // 最大缓存条数 enableTextExtract: true, // 是否从文本内容中提取URL enableHtmlEntityDecode: true,// 是否解码HTML实体 enableFallbackResolver: true,// 是否启用网络请求兜底解析 // 需要排除的特殊协议 excludedProtocols: [ 'javascript:', 'mailto:', 'tel:', 'sms:', '', 'about:', 'blob:', 'file:', 'ftp:' ], enableClickIntercept: true, // 是否拦截点击事件直接跳转 clickInterceptPriority: 'capture', // 点击事件拦截阶段 enableVisitedMark: true, // 是否标记已处理的链接 visitedMarkStyle: { opacity: '0.5' }, // 已处理链接的样式 // 不进行拦截的元素选择器 excludeClickSelectors: [ '[data-no-redirect]', '[role="button"]', '.no-redirect', '#no-redirect', 'button', 'input[type="button"]', 'input[type="submit"]' ], enableTrackingCleanup: true, // 是否清理追踪参数 enableCustomAttrCleanup: false, // 是否清理自定义追踪属性 fallbackMemoryLimit: 200, // 兜底解析内存缓存限制 autoJumpOnSelf: true // 当前页面如果是重定向页是否自动跳转 }; // 读取并合并用户自定义配置 try { const userConfig = GM_getValue('redirectConfig', {}); if (userConfig && typeof userConfig === 'object') { for (const key in userConfig) { if (typeof CONFIG[key] === 'number') { CONFIG[key] = Number(userConfig[key]) || CONFIG[key]; } else if (typeof CONFIG[key] === 'boolean') { CONFIG[key] = !!userConfig[key]; } else { CONFIG[key] = userConfig[key]; } } } } catch (e) {} // 缓存与存储相关的键名 const URL_CACHE_KEY = 'urlResolveCache'; const DOMAIN_RESOLVER_MAP_KEY = 'domainResolverMap'; const RESOLVER_STATS_KEY = 'resolverStats'; // 缓存时间与容量限制计算 const CACHE_TTL = CONFIG.cacheExpireDays * 24 * 60 * 60 * 1000; const MAX_CACHE_SIZE = CONFIG.maxCacheSize; const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // 可疑的URL路径正则(通常是跳转中间页的路径特征) const SUSPICIOUS_PATH_REGEX = /\/(?:go-wild|linkfilter|link-jump|linkjump|safecheck|r\/goto|scenario\/link|office\/link|action\/GoToLink|community\/middlepage|developer\/tools|mo\/q)(?:[/?]|$)|\/(?:go|out|redirect|link2?|forward|to|jump|jump\.php|outlink|transfer)(?:[/?]|\.html?|$)/i; // 可疑的URL参数正则(通常包含真实目标URL的参数名) const SUSPICIOUS_PARAM_REGEX = /[?&](url|u|redirect|goto|to|dest|link|target|out|go|jump|redirect_url|redirect_uri|redirect_to|forward|pfurl)=/i; // 通用URL提取正则 const URL_REGEX = /https?:\/\/[^\s"'<>\]\[{}|\\^`]+/gi; // 追踪参数名正则 const TRACKING_REGEX = /^(spm|from_|ref_|track|trk|share_|embeds_|refer_)|_from$|scm|referrer/i; // Base64编码特征正则 const BASE64_REGEX = /^[A-Za-z0-9+/=]+$/; // HTML实体特征正则 const HTML_ENTITY_REGEX = /&[a-z]+;|&#\d+;/; // URI编码特征正则 const URI_ENCODE_REGEX = /%[0-9A-F]{2}/i; // 常见的可能包含目标URL的参数名 const COMMON_PARAM_NAMES = [ 'url', 'u', 'target', 'to', 'dest', 'href', 'link', 'goto', 'redirect', 'redirect_url', 'redirect_uri', 'redirect_to', 'return_url', 'return_to', 'next', 'forward', 'fwd', 'out', 'go', 'jump', 'redirectUrl', 'redirectURI', 'gourl', 'q', 'ru', 'path', 'continue', 'return', 'redir', 'destination', 'dest_url', 'link_url', 'goto_url', '_url', 'service_url', 'referer', 'next_url', 'back_url', 'callback', 'pfurl', 'original_url' ]; // 需要清理的常见追踪参数名 const TRACKING_PARAMS = [ 'spm', 'mkt', 'src', 'from', 'source', 'alias', 'vd_source', 'brand', 'curator_clanid', 'snr', 'redir', 'sprefix', 'utm_id', 'utm_content', 'utm_source', 'utm_medium', 'utm_sources', 'utm_term', 'utm_campaign', 'utm_referrer', 'utm_keyword', 'ref', 'feature', 'track', 'trk', 'share_', 'embeds_', 'refer_', '_from', 'scm', 'cvid', 'FORM', 'PC', 'dib', 'tn', 'rsv_dl', 'rsv_pq', 'ie', 'rsv_tid', 'sa', 'ck', 'vl' ]; // 常见的存放真实URL的自定义HTML属性 const COMMON_REDIRECT_ATTRS = [ 'data-href', 'data-url', 'data-link', 'data-target', 'data-mdurl', 'rl-link-href', 'mu', 'data-redirect', 'data-dest', 'data-to', 'data-goto', 'data-permalink', 'data-real-url', 'data-original', 'data-ext', 'data-cthref', 'data-original-url', 'data-jump', 'data-lk', 'data-out', 'data-src', 'data-uri', 'data-canonical', 'data-resource', 'data-request-url', 'data-external-link', 'data-true-href', 'data-actual-url', 'data-final-url', 'data-destination', 'data-redirect-url', 'data-redirect-target', 'data-link-url', 'hrefdata', 'realhref' ]; // 常见的短链接域名 const SHORTENER_DOMAINS = [ 't.co', 't.cn', 'bit.ly', 'goo.gl', 'ow.ly', 'tinyurl.com', 'is.gd', 'buff.ly', 'db.tt', 'j.mp', 'amzn.to', 'ebay.to', 'wp.me', 'tiny.cc', 'shorte.st', 'adf.ly' ]; // 常见的触发跳转的onclick代码特征 const REDIRECT_ONCLICK_PATTERNS = [ 'showUrlAlert', 'window.open', 'location.href', 'location.replace' ]; /** * 验证是否为合法的 HTTP(S) URL */ function isValidUrl(str) { if (!str || typeof str !== 'string') return false; try { const u = new URL(str); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; } } /** * 解码 Base64 字符串 * 兼容部分平台去掉尾部 '=' 或添加 'a1' 前缀的情况 */ function base64Decode(str) { let base64 = str; if (base64.startsWith('a1')) { base64 = base64.slice(2); } base64 = base64.replace(/-/g, '+').replace(/_/g, '/'); while (base64.length % 4) base64 += '='; return atob(base64); } /** * 判断是否为已知的短链接域名 */ function isShortenerDomain(url) { try { const hostname = new URL(url).hostname.toLowerCase(); return SHORTENER_DOMAINS.some(d => hostname === d || hostname.endsWith('.' + d)); } catch { return false; } } /** * 智能多层解码 (URI -> Base64 -> HTML Entity) * 自动尝试不同的解码方式直到无法继续解码 */ function smartDecode(str) { if (!str || typeof str !== 'string') return str; let current = str; let maxLoops = 3; while (maxLoops-- > 0) { let decoded = null; // 尝试 URI 解码 if (URI_ENCODE_REGEX.test(current)) { try { decoded = decodeURIComponent(current); } catch {} } // 尝试 Base64 解码 if (!decoded) { try { let test = current; if (test.startsWith('a1')) test = test.slice(2); test = test.replace(/-/g, '+').replace(/_/g, '/'); while (test.length % 4) test += '='; if (BASE64_REGEX.test(test) && test.length >= 4) { decoded = atob(test); } } catch {} } // 尝试 HTML 实体解码 if (!decoded && HTML_ENTITY_REGEX.test(current)) { try { const textarea = document.createElement('textarea'); textarea.innerHTML = current; decoded = textarea.value; } catch {} } if (!decoded || decoded === current) break; current = decoded; } return current; } /** * 按指定类型顺序尝试解码 */ function tryDecode(value, types = ['uri', 'base64', 'html']) { for (const type of types) { try { if (type === 'uri') return decodeURIComponent(value); else if (type === 'base64') return base64Decode(value); else if (type === 'html') { const textarea = document.createElement('textarea'); textarea.innerHTML = value; return textarea.value; } } catch {} } return value; } /** * 从纯文本中提取第一个符合规则的 URL */ function extractUrlFromText(text) { if (!text) return null; const match = text.match(URL_REGEX); return match ? match[0] : null; } /** * 提取 URL 的域名 */ function extractDomain(url) { try { return new URL(url).hostname; } catch { return null; } } /** * 清理 URL 中的追踪参数 */ function cleanUrlParams(url) { try { const urlObj = new URL(url); const params = urlObj.searchParams; TRACKING_PARAMS.forEach(p => params.delete(p)); Array.from(params.keys()).forEach(key => { if (TRACKING_REGEX.test(key)) params.delete(key); }); return urlObj.href; } catch { return url; } } // URL 对象内存缓存,避免频繁 new URL() const urlObjectCache = new Map(); const MAX_URL_OBJ_CACHE = 200; function getUrlObject(url) { if (urlObjectCache.has(url)) return urlObjectCache.get(url); try { const urlObj = new URL(url); if (urlObjectCache.size >= MAX_URL_OBJ_CACHE) { const firstKey = urlObjectCache.keys().next().value; urlObjectCache.delete(firstKey); } urlObjectCache.set(url, urlObj); return urlObj; } catch { return null; } } /** * 阻止事件冒泡和默认行为 */ function preventEvent(event) { if (!event) return; if (event.preventDefault) event.preventDefault(); if (event.stopPropagation) event.stopPropagation(); event.cancelBubble = true; event.returnValue = false; } /** * 判断点击的元素是否应该被排除拦截 */ function shouldExcludeClick(element) { for (const selector of CONFIG.excludeClickSelectors) { if (element.matches?.(selector) || element.closest?.(selector)) { return true; } } if (element.hasAttribute('data-no-redirect-intercept')) return true; return false; } /** * 标记元素已被处理并应用样式 */ function markAsVisited(element) { if (!CONFIG.enableVisitedMark) return; element.setAttribute('data-redirect-visited', 'true'); if (CONFIG.visitedMarkStyle) { Object.assign(element.style, CONFIG.visitedMarkStyle); } } /** * URL 解析结果缓存模块 * 使用内存缓存 + 批量写入 GM 存储的策略,减少 IO 阻塞 */ const urlCache = { memCache: new Map(), pendingWrites: new Map(), pendingDeletes: new Set(), writeTimer: null, // 初始化时从 GM 存储加载到内存 load() { if (!CONFIG.enableCache) return; try { const cache = GM_getValue(URL_CACHE_KEY, {}); for (const [key, value] of Object.entries(cache)) { if (value && value.timestamp) { this.memCache.set(key, value); } } } catch (e) {} }, // 读取缓存 get(originalUrl) { if (!CONFIG.enableCache) return null; const item = this.memCache.get(originalUrl); if (item && Date.now() - item.timestamp < CACHE_TTL) { return item.url; } // 如果过期,加入待删除队列 if (item) { this.memCache.delete(originalUrl); this.pendingDeletes.add(originalUrl); this.scheduleWrite(); } return null; }, // 写入缓存 set(originalUrl, realUrl) { if (!CONFIG.enableCache || !realUrl) return; if (originalUrl === realUrl) return; const item = { url: realUrl, timestamp: Date.now() }; this.memCache.set(originalUrl, item); this.pendingWrites.set(originalUrl, item); this.pendingDeletes.delete(originalUrl); this.scheduleWrite(); // 超过最大限制时,淘汰最旧的缓存 if (this.memCache.size > MAX_CACHE_SIZE) { const oldestKey = this._findOldestKey(); if (oldestKey) { this.memCache.delete(oldestKey); this.pendingWrites.delete(oldestKey); this.pendingDeletes.add(oldestKey); } } }, // 删除指定缓存 delete(originalUrl) { this.memCache.delete(originalUrl); this.pendingWrites.delete(originalUrl); this.pendingDeletes.add(originalUrl); this.scheduleWrite(); }, // 延迟批量写入,避免频繁调用 GM_setValue scheduleWrite() { if (this.writeTimer) clearTimeout(this.writeTimer); this.writeTimer = setTimeout(() => this.flush(), 5000); }, // 执行批量写入和删除 flush() { if (this.pendingWrites.size === 0 && this.pendingDeletes.size === 0) return; try { const cache = GM_getValue(URL_CACHE_KEY, {}); for (const [key, value] of this.pendingWrites) { cache[key] = value; } for (const key of this.pendingDeletes) { delete cache[key]; } GM_setValue(URL_CACHE_KEY, cache); this.pendingWrites.clear(); this.pendingDeletes.clear(); } catch (e) {} this.writeTimer = null; }, // 定时清理过期缓存 clearExpired() { const now = Date.now(); let changed = false; for (const [key, item] of this.memCache) { if (now - item.timestamp >= CACHE_TTL) { this.memCache.delete(key); this.pendingWrites.delete(key); this.pendingDeletes.add(key); changed = true; } } if (changed) this.scheduleWrite(); }, // 清理所有缓存 clearAll() { this.memCache.clear(); this.pendingWrites.clear(); this.pendingDeletes.clear(); if (this.writeTimer) clearTimeout(this.writeTimer); GM_setValue(URL_CACHE_KEY, {}); }, // 获取缓存统计信息 getStats() { const now = Date.now(); let valid = 0, expired = 0; for (const item of this.memCache.values()) { if (now - item.timestamp < CACHE_TTL) valid++; else expired++; } return { total: this.memCache.size, valid, expired }; }, // 内部辅助:找到最旧的缓存键 _findOldestKey() { let oldestKey = null; let oldestTime = Date.now(); for (const [key, item] of this.memCache) { if (item.timestamp < oldestTime) { oldestTime = item.timestamp; oldestKey = key; } } return oldestKey; } }; // 初始化加载并设置定时清理 urlCache.load(); setInterval(() => urlCache.clearExpired(), CACHE_CLEANUP_INTERVAL); /** * 解析器成功率统计模块 * 用于记录不同解析器在不同域名下的表现,优化解析顺序 */ const resolverStats = { // 记录一次解析结果 record(domain, resolverName, success) { if (!domain || !resolverName) return; try { const stats = GM_getValue(RESOLVER_STATS_KEY, {}); if (!stats[domain]) stats[domain] = {}; if (!stats[domain][resolverName]) { stats[domain][resolverName] = { success: 0, total: 0 }; } stats[domain][resolverName].total++; if (success) stats[domain][resolverName].success++; GM_setValue(RESOLVER_STATS_KEY, stats); } catch (e) {} }, // 获取指定域名下成功率最高的解析器 getBest(domain) { if (!domain) return null; try { const stats = GM_getValue(RESOLVER_STATS_KEY, {}); const domainStats = stats[domain]; if (!domainStats) return null; let best = null; let bestRate = 0; for (const [name, stat] of Object.entries(domainStats)) { // 至少尝试过3次才纳入统计,避免偶然性 if (stat.total >= 3) { const rate = stat.success / stat.total; if (rate > bestRate) { bestRate = rate; best = name; } } } return best; } catch { return null; } }, clearAll() { GM_setValue(RESOLVER_STATS_KEY, {}); } }; /** * 域名到特定解析器的映射模块 * 强制指定某个域名使用某个解析器 */ const domainResolverMap = { get() { return GM_getValue(DOMAIN_RESOLVER_MAP_KEY, {}); }, set(domain, resolverName) { if (!domain || !resolverName) return; const map = this.get(); map[domain] = resolverName; GM_setValue(DOMAIN_RESOLVER_MAP_KEY, map); }, delete(domain) { const map = this.get(); if (map[domain]) { delete map[domain]; GM_setValue(DOMAIN_RESOLVER_MAP_KEY, map); } }, clearAll() { GM_setValue(DOMAIN_RESOLVER_MAP_KEY, {}); } }; /** * 解析器基类 */ class RedirectResolver { constructor(name) { this.name = name || 'BaseResolver'; } canResolve(element) { return false; } resolve(element) { return null; } isValidUrl(str) { return isValidUrl(str); } } /** * 自定义属性解析器 * 从 data-href, data-url 等属性中提取真实链接 */ class AttributeResolver extends RedirectResolver { constructor(extraAttrs = []) { super('AttributeResolver'); this.attributeNames = [...COMMON_REDIRECT_ATTRS, ...extraAttrs]; } canResolve(element) { if (element.hasAttribute(REDIRECT_COMPLETED)) return false; return this.attributeNames.some(attr => element.hasAttribute(attr)); } resolve(element) { for (const attr of this.attributeNames) { const value = element.getAttribute(attr); if (value) { const decoded = smartDecode(value); if (this.isValidUrl(decoded)) return decoded; if (this.isValidUrl(value)) return value; } } return null; } } /** * URL 参数解析器 * 从 ?url=xxx 或 &target=xxx 中提取 */ class QueryParamResolver extends RedirectResolver { constructor(paramNames) { super('QueryParamResolver'); this.paramNames = Array.isArray(paramNames) ? paramNames : [paramNames]; } canResolve(element) { if (element.hasAttribute(REDIRECT_COMPLETED)) return false; const search = element.search; if (!search) return false; return this.paramNames.some(param => search.includes('?' + param + '=') || search.includes('&' + param + '=')); } resolve(element) { try { const urlObj = getUrlObject(element.href); if (!urlObj) return null; for (const param of this.paramNames) { const rawValue = urlObj.searchParams.get(param); if (!rawValue) continue; const decoded = smartDecode(rawValue); if (this.isValidUrl(decoded)) return decoded; } } catch (e) {} return null; } } /** * 正则匹配解析器 * 使用自定义正则从 href 中提取 */ class RegexResolver extends RedirectResolver { constructor(regex, matchIndex = 1, needDecode = true) { super('RegexResolver'); this.regex = regex; this.matchIndex = matchIndex; this.needDecode = needDecode; } canResolve(element) { if (element.hasAttribute(REDIRECT_COMPLETED)) return false; return this.regex.test(element.href); } resolve(element) { const match = this.regex.exec(element.href); if (!match || !match[this.matchIndex]) return null; let realUrl = match[this.matchIndex]; if (this.needDecode) realUrl = smartDecode(realUrl); return this.isValidUrl(realUrl) ? realUrl : null; } } /** * 文本内容解析器 * 针对短链接等,提取元素内部文本中的真实URL */ class TextContentResolver extends RedirectResolver { constructor() { super('TextContentResolver'); } canResolve(element) { if (element.hasAttribute(REDIRECT_COMPLETED)) return false; const href = element.href; if (!href) return false; if (!CONFIG.enableTextExtract) return false; if (isShortenerDomain(href)) return true; const text = element.innerText?.trim(); if (!text) return false; return URL_REGEX.test(text); } resolve(element) { const text = element.innerText?.trim(); if (!text) return null; const url = extractUrlFromText(text); if (url && this.isValidUrl(url)) { if (url !== element.href) return url; if (isShortenerDomain(element.href)) { const cleaned = url.replace(/\u2026/g, '').trim(); // 去除省略号 if (this.isValidUrl(cleaned)) return cleaned; } } return null; } } /** * 路径特征解析器 * 解析类似 /go/xxx, /redirect/xxx 的路径结构 */ class PathResolver extends RedirectResolver { constructor() { super('PathResolver'); } canResolve(element) { const path = element.pathname; return SUSPICIOUS_PATH_REGEX.test(path); } resolve(element) { try { const path = element.pathname; // 复杂路径匹配 const compoundMatch = path.match(/^\/(?:go-wild|linkfilter|link-jump|linkjump|safecheck|r\/goto|scenario\/link|office\/link|action\/GoToLink|community\/middlepage|developer\/tools|mo\/q)\/?(.+)/i); if (compoundMatch && compoundMatch[1]) { let target = decodeURIComponent(compoundMatch[1]); if (!target.startsWith('http')) target = 'https://' + target; if (this.isValidUrl(target)) return target; } // 通用路径匹配 const genericMatch = path.match(/^\/(?:go|out|redirect|link2?|forward|to|jump|jump\.php|outlink|transfer)(?:\/([^\s?]+))?/i); if (genericMatch) { let target = genericMatch[1]; // 如果路径没带目标,尝试从参数中取 if (!target) { const urlObj = getUrlObject(element.href); if (urlObj) { for (const name of COMMON_PARAM_NAMES) { const val = urlObj.searchParams.get(name); if (val) { target = smartDecode(val); break; } } } } if (target) { if (!target.startsWith('http')) target = 'https://' + target; if (this.isValidUrl(target)) return target; } } } catch {} return null; } } /** * 分隔符解析器 * 处理特定格式的拼接,如 GoToLink?url=xxx */ class SeparatorResolver extends RedirectResolver { constructor() { super('SeparatorResolver'); this.separators = [/GoToLink\?url=/i, /go\?to=/i, /jump\?to=/i]; } canResolve(element) { if (element.hasAttribute(REDIRECT_COMPLETED)) return false; return this.separators.some(sep => sep.test(element.href)); } resolve(element) { for (const sep of this.separators) { const idx = element.href.search(sep); if (idx !== -1) { const part = element.href.slice(idx); const eqIdx = part.indexOf('='); if (eqIdx !== -1) { const value = part.slice(eqIdx + 1).split('&')[0]; const decoded = smartDecode(decodeURIComponent(value)); if (this.isValidUrl(decoded)) return decoded; } } } return null; } } /** * 增强型兜底解析器 (异步) * 当其他规则无法解析时,发起真实的网络请求 (HEAD/GET) 获取最终 302 跳转地址 */ class EnhancedFallbackResolver extends RedirectResolver { constructor() { super('EnhancedFallbackResolver'); this.requestCache = new Map(); this.pendingRequests = new Map(); this.requestQueue = []; this.maxConcurrent = CONFIG.maxConcurrentRequests; this.maxCacheSize = CONFIG.fallbackMemoryLimit || 200; } canResolve(element) { if (!CONFIG.enableFallbackResolver) return false; return !element.hasAttribute(REDIRECT_COMPLETED); } async resolve(element) { const href = element.href; if (this.requestCache.has(href)) return this.requestCache.get(href); const cached = urlCache.get(href); if (cached) { this.requestCache.set(href, cached); return cached; } // 防止重复请求同一个URL if (this.pendingRequests.has(href)) { return this.pendingRequests.get(href); } // 控制并发数 while (this.pendingRequests.size >= this.maxConcurrent) { await new Promise(resolve => this.requestQueue.push(resolve)); } const promise = this.performRequest(href); this.pendingRequests.set(href, promise); try { const result = await promise; if (result) { this.requestCache.set(href, result); urlCache.set(href, result); this._limitCache(); } return result; } finally { this.pendingRequests.delete(href); if (this.requestQueue.length > 0) { const next = this.requestQueue.shift(); next(); } } } _limitCache() { if (this.requestCache.size <= this.maxCacheSize) return; const keys = Array.from(this.requestCache.keys()); const removeCount = keys.length - this.maxCacheSize; for (let i = 0; i < removeCount; i++) { this.requestCache.delete(keys[i]); } } async performRequest(href) { let finalUrl = await this._request('GET', href); if (finalUrl && finalUrl !== href) return finalUrl; return null; } _request(method, url) { return new Promise(resolve => { const timeoutId = setTimeout(() => resolve(null), CONFIG.requestTimeout); GM.xmlHttpRequest({ method: method, url: url, anonymous: true, headers: { 'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8', 'User-Agent': navigator.userAgent, 'Range': 'bytes=0-0' // 只请求0字节,节省流量,依赖响应头中的 Location }, onload: (resp) => { clearTimeout(timeoutId); // 优先使用最终响应URL if (resp.finalUrl && resp.finalUrl !== url && isValidUrl(resp.finalUrl)) { resolve(resp.finalUrl); return; } // 尝试从 HTML 中的 Meta Refresh 标签提取 if (resp.responseText) { const metaUrl = this._extractMetaRefresh(resp.responseText); if (metaUrl && isValidUrl(metaUrl)) { resolve(metaUrl); return; } } resolve(null); }, onerror: () => { clearTimeout(timeoutId); resolve(null); }, ontimeout: () => { clearTimeout(timeoutId); resolve(null); } }); }); } _extractMetaRefresh(html) { if (!html) return null; const metaMatch = html.match(/]*http-equiv=["']refresh["'][^>]*content=["']\d*;\s*url=([^"']+)["']/i); if (metaMatch && metaMatch[1]) { let url = metaMatch[1].trim(); // 处理相对路径 if (url.startsWith('/')) { try { const base = new URL(window.location.href); url = base.origin + url; } catch {} } return url; } return null; } } /** * 智能组合解析器 * 统筹调度所有的子解析器,根据统计和历史记录优化解析顺序 */ class SmartCompositeResolver extends RedirectResolver { constructor(resolvers) { super('SmartCompositeResolver'); this.resolvers = resolvers || []; // 分离同步和异步解析器 this.syncResolvers = this.resolvers.filter(r => !(r instanceof EnhancedFallbackResolver)); } addResolver(resolver) { this.resolvers.push(resolver); if (!(resolver instanceof EnhancedFallbackResolver)) { this.syncResolvers.push(resolver); } } canResolve(element) { return this.resolvers.some(r => r.canResolve(element)); } getResolverByName(name) { return this.resolvers.find(r => r.name === name); } // 同步解析方法 (用于点击拦截等需要极致速度的场景) resolveSync(element) { const href = element.href; const domain = extractDomain(href); // 优先使用历史统计或用户指定映射的最佳解析器 const preferredName = resolverStats.getBest(domain) || (domain && domainResolverMap.get()[domain]); if (preferredName) { const preferred = this.getResolverByName(preferredName); if (preferred && !(preferred instanceof EnhancedFallbackResolver) && preferred.canResolve(element)) { try { const result = preferred.resolve(element); if (result && this.isValidUrl(result)) { resolverStats.record(domain, preferred.name, true); return { url: result, resolverName: preferred.name }; } resolverStats.record(domain, preferred.name, false); } catch {} } } // 按顺序尝试其他同步解析器 for (const resolver of this.syncResolvers) { if (resolver.name === preferredName) continue; if (resolver.canResolve(element)) { try { const result = resolver.resolve(element); if (result && this.isValidUrl(result)) { resolverStats.record(domain, resolver.name, true); return { url: result, resolverName: resolver.name }; } resolverStats.record(domain, resolver.name, false); } catch {} } } return { url: null, resolverName: null }; } // 异步解析方法 (包含网络请求兜底) async resolve(element, preferredResolverName = null) { const href = element.href; const domain = extractDomain(href); if (preferredResolverName) { const preferred = this.getResolverByName(preferredResolverName); if (preferred && preferred.canResolve(element)) { try { const result = await preferred.resolve(element); if (result && this.isValidUrl(result)) { resolverStats.record(domain, preferred.name, true); return { url: result, resolverName: preferred.name }; } else { resolverStats.record(domain, preferred.name, false); } } catch (e) { resolverStats.record(domain, preferred.name, false); } } } for (const resolver of this.resolvers) { if (resolver.name === preferredResolverName) continue; if (resolver.canResolve(element)) { try { const result = await resolver.resolve(element); if (result && this.isValidUrl(result)) { resolverStats.record(domain, resolver.name, true); return { url: result, resolverName: resolver.name }; } else { resolverStats.record(domain, resolver.name, false); } } catch (e) { resolverStats.record(domain, resolver.name, false); } } } return { url: null, resolverName: null }; } } /** * 创建并组装默认的解析器链 */ function createDefaultResolver() { const composite = new SmartCompositeResolver(); composite.addResolver(new AttributeResolver()); composite.addResolver(new QueryParamResolver(COMMON_PARAM_NAMES)); composite.addResolver(new SeparatorResolver()); composite.addResolver(new RegexResolver(/(?:https?:\/\/[^\/]+)?\/[^?]*[?&](?:url|u|target|to|dest|href|link|goto|redirect)=([^&]+)/i, 1, true)); composite.addResolver(new PathResolver()); if (CONFIG.enableTextExtract) { composite.addResolver(new TextContentResolver()); } // 兜底解析器必须放在最后 composite.addResolver(new EnhancedFallbackResolver()); return composite; } /** * 链接清理器 * 移除 a 标签上的跟踪属性 (如 ping, 特定 onclick 等) */ class LinkCleaner { constructor() { this.processed = new WeakSet(); } clean(element) { if (this.processed.has(element)) return; if (!CONFIG.enableTrackingCleanup) return; if (!(element instanceof HTMLAnchorElement)) return; this.processed.add(element); // 移除 ping 追踪 if (element.hasAttribute('ping')) { element.removeAttribute('ping'); } // 移除包含跳转代码的 onclick if (element.hasAttribute('onclick')) { const onclick = element.getAttribute('onclick'); if (REDIRECT_ONCLICK_PATTERNS.some(p => onclick.includes(p))) { element.removeAttribute('onclick'); } } // 移除鼠标事件追踪 for (const attr of ['onmouseover', 'onmouseout']) { if (element.hasAttribute(attr)) { const val = element.getAttribute(attr); if (REDIRECT_ONCLICK_PATTERNS.some(p => val.includes(p))) { element.removeAttribute(attr); } } } // 移除其他自定义追踪属性 if (CONFIG.enableCustomAttrCleanup) { element.removeAttribute('data-rw'); element.removeAttribute('data-al'); } } // 监听 DOM 变化,自动清理新增的 a 标签 observe(doc) { const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.target instanceof HTMLAnchorElement) { this.clean(mutation.target); } else if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (node instanceof HTMLAnchorElement) this.clean(node); else if (node.querySelectorAll) { node.querySelectorAll('a').forEach(a => this.clean(a)); } } } } } }); observer.observe(doc, { childList: true, subtree: true, attributeFilter: ['ping', 'onclick', 'onmouseover', 'onmouseout', 'data-rw', 'data-al'] }); doc.querySelectorAll('a').forEach(a => this.clean(a)); } } /** * 防抖函数工具 */ function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } /** * 主应用控制类 * 负责扫描页面、拦截点击、调度解析器和修改链接 */ class RedirectApp { constructor() { this.config = CONFIG; this.resolver = createDefaultResolver(); this.linkCleaner = new LinkCleaner(); this.processingElements = new WeakSet(); this.mutationObserver = null; this.scanDebounce = debounce(this._scanNewLinks.bind(this), 500); this.clickHandlerBound = this._handleClick.bind(this); } // 判断一个链接是否值得被处理 shouldProcessLink(element) { const href = element.href; if (!href) return false; const protocol = element.protocol?.toLowerCase(); if (protocol && this.config.excludedProtocols.includes(protocol)) return false; if (href === '#' || href === 'javascript:void(0);' || href === 'about:blank') return false; if (this.config.excludeSameOrigin) { if (element.hostname === location.hostname) return false; } const search = element.search; // 命中可疑特征即需处理 if (search && SUSPICIOUS_PARAM_REGEX.test(search)) return true; if (SUSPICIOUS_PATH_REGEX.test(element.pathname)) return true; if (COMMON_REDIRECT_ATTRS.some(attr => element.hasAttribute(attr))) return true; if (isShortenerDomain(href)) return true; return false; } // 执行页面跳转 _navigateTo(url, originalElement) { if (!url) return; try { const targetAttr = originalElement.getAttribute('target'); if (targetAttr === '_blank') { const opened = window.open(url, '_blank'); // 如果被拦截,退回到当前页面跳转 if (!opened || opened.closed) { location.href = url; } } else { location.href = url; } } catch (e) { location.href = url; } } // 核心点击拦截逻辑 async _handleClick(event) { if (!this.config.enableClickIntercept) return; // 忽略非左键或带有修饰键的点击 if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; // 查找被点击的 a 标签 const path = event.composedPath ? event.composedPath() : []; let targetElement = event.target; for (const node of path) { if (node instanceof Element) { if (node.tagName === 'A' || node.getAttribute?.('role') === 'link') { targetElement = node; break; } } } if (shouldExcludeClick(targetElement)) return; if (!this.shouldProcessLink(targetElement)) return; // 防止重复拦截 if (targetElement.hasAttribute(CLICK_HANDLED)) return; targetElement.setAttribute(CLICK_HANDLED, 'true'); const originalHref = targetElement.href; preventEvent(event); // 1. 尝试从缓存读取 const cachedUrl = urlCache.get(originalHref); if (cachedUrl && isValidUrl(cachedUrl) && cachedUrl !== originalHref) { markAsVisited(targetElement); this._navigateTo(cachedUrl, targetElement); return; } // 2. 尝试同步解析 (极速响应) const syncResult = this.resolver.resolveSync(targetElement); if (syncResult.url && isValidUrl(syncResult.url) && syncResult.url !== originalHref) { const finalUrl = cleanUrlParams(syncResult.url); urlCache.set(originalHref, finalUrl); const domain = extractDomain(originalHref); if (domain && syncResult.resolverName) { domainResolverMap.set(domain, syncResult.resolverName); } markAsVisited(targetElement); this._navigateTo(finalUrl, targetElement); return; } // 3. 尝试异步解析 (可能包含网络请求,受超时限制) try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), this.config.maxProcessTime)); const domain = extractDomain(originalHref); const domainMap = domainResolverMap.get(); const preferredFromMap = domain ? domainMap[domain] : null; const bestFromStats = resolverStats.getBest(domain); const preferredResolverName = bestFromStats || preferredFromMap; const { url: realUrl } = await Promise.race([ this.resolver.resolve(targetElement, preferredResolverName), timeoutPromise ]); if (realUrl && isValidUrl(realUrl) && realUrl !== originalHref) { const finalUrl = cleanUrlParams(realUrl); urlCache.set(originalHref, finalUrl); markAsVisited(targetElement); this._navigateTo(finalUrl, targetElement); return; } } catch (error) {} // 4. 解析失败,放行原始链接 this._navigateTo(originalHref, targetElement); } // 处理单个链接 (静默替换 href) async processLink(element) { if (this.processingElements.has(element)) return; if (element.hasAttribute(REDIRECT_COMPLETED)) return; if (!this.shouldProcessLink(element)) return; this.processingElements.add(element); try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), this.config.maxProcessTime)); const originalHref = element.href; // 查缓存 const cachedUrl = urlCache.get(originalHref); if (cachedUrl && cachedUrl !== originalHref && isValidUrl(cachedUrl)) { element.href = cachedUrl; element.setAttribute(REDIRECT_COMPLETED, 'true'); return; } // 获取首选解析器 const domain = extractDomain(originalHref); const domainMap = domainResolverMap.get(); const preferredFromMap = domain ? domainMap[domain] : null; const bestFromStats = resolverStats.getBest(domain); const preferredResolverName = bestFromStats || preferredFromMap; // 执行解析 const { url: realUrl, resolverName } = await Promise.race([ this.resolver.resolve(element, preferredResolverName), timeoutPromise ]); if (realUrl && realUrl !== originalHref && isValidUrl(realUrl)) { // 如果解析出来的还是个中间链接,递归解析 const finalUrl = await this.resolveDeep(realUrl, 0, domain, new Set([originalHref])); const cleanedUrl = cleanUrlParams(finalUrl); element.href = cleanedUrl; element.setAttribute(REDIRECT_COMPLETED, 'true'); urlCache.set(originalHref, cleanedUrl); if (domain && resolverName) { domainResolverMap.set(domain, resolverName); } } } catch (error) { } finally { this.processingElements.delete(element); } } // 递归解析多层重定向 async resolveDeep(url, depth = 0, domain = null, visited = null) { if (depth >= this.config.maxResolveDepth) return url; if (visited && visited.has(url)) return url; if (visited) visited.add(url); const cached = urlCache.get(url); if (cached) return cached; const a = document.createElement('a'); a.href = url; const { url: newUrl } = await this.resolver.resolve(a, null); if (!newUrl || newUrl === url || !isValidUrl(newUrl)) return url; const result = await this.resolveDeep(newUrl, depth + 1, extractDomain(newUrl), visited); if (result !== url) urlCache.set(url, result); return result; } // 扫描新增的链接 _scanNewLinks() { const sel = 'a:not([' + REDIRECT_COMPLETED + '])'; document.querySelectorAll(sel).forEach(link => this.processLink(link)); } // DOM 变更回调 handleMutation = (mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; if (this.isLinkElement(node)) { this.processLink(node); } else if (node.querySelectorAll) { const sel = 'a:not([' + REDIRECT_COMPLETED + '])'; node.querySelectorAll(sel).forEach(aNode => this.processLink(aNode)); } } } else if (mutation.type === 'attributes') { if (this.isLinkElement(mutation.target)) { this.processLink(mutation.target); } } } }; isLinkElement(node) { return ( node instanceof HTMLAnchorElement || (node.nodeType === 1 && node.tagName === 'A' && node.namespaceURI === 'http://www.w3.org/1999/svg') ); } // 分批处理链接,避免阻塞主线程 processLinksInBatches(links, batchSize = 20) { let index = 0; const processBatch = () => { const end = Math.min(index + batchSize, links.length); for (; index < end; index++) { this.processLink(links[index]); } if (index < links.length) { if (window.requestIdleCallback) { requestIdleCallback(processBatch, { timeout: 1000 }); } else { setTimeout(processBatch, 10); } } }; processBatch(); } // 启动主程序 start() { // 注册点击拦截 if (this.config.enableClickIntercept) { const useCapture = this.config.clickInterceptPriority === 'capture'; document.addEventListener('click', this.clickHandlerBound, { capture: useCapture, passive: false }); } // 初始化处理当前页面的所有链接 const links = Array.from(document.querySelectorAll('a')); this.processLinksInBatches(links); // 启动链接清理器 this.linkCleaner.observe(document); // 监听后续动态插入的链接 this.mutationObserver = new MutationObserver(this.handleMutation); this.mutationObserver.observe(document, { childList: true, subtree: true, attributeFilter: ['href', 'data-href', 'data-url', ...COMMON_REDIRECT_ATTRS] }); } // 停止主程序 stop() { if (this.mutationObserver) this.mutationObserver.disconnect(); if (this.config.enableClickIntercept) { document.removeEventListener('click', this.clickHandlerBound, { capture: this.config.clickInterceptPriority === 'capture' }); } } } /** * 当前页面自动跳转应用 * 如果当前打开的页面本身就是个重定向中间页,直接跳走 */ class AutoJumpApp { constructor() { this.commonParams = COMMON_PARAM_NAMES; this.maxDepth = CONFIG.maxResolveDepth; } async resolveDeep(url, depth = 0, visited = null) { if (depth >= this.maxDepth) return url; if (visited && visited.has(url)) return url; if (visited) visited.add(url); const cached = urlCache.get(url); if (cached) return cached; try { const urlObj = getUrlObject(url); if (!urlObj) return url; for (const param of this.commonParams) { const rawValue = urlObj.searchParams.get(param); if (!rawValue) continue; const decoded = smartDecode(rawValue); if (isValidUrl(decoded) && decoded !== url) { const final = await this.resolveDeep(decoded, depth + 1, visited); if (final !== url) { urlCache.set(url, final); return final; } } } } catch (e) {} return url; } async bootstrap() { try { const currentUrl = location.href; if (!CONFIG.autoJumpOnSelf) return false; const pathname = location.pathname; const search = location.search; // 判断当前页面是否符合中间页特征 let isRedirectPage = SUSPICIOUS_PATH_REGEX.test(pathname) || SUSPICIOUS_PARAM_REGEX.test(search); if (!isRedirectPage) return false; const finalUrl = await this.resolveDeep(currentUrl, 0, new Set([currentUrl])); if (finalUrl !== currentUrl && isValidUrl(finalUrl)) { const cleaned = cleanUrlParams(finalUrl); console.log('Auto-jump: ' + currentUrl + ' -> ' + cleaned); location.replace(cleaned); return true; } } catch (e) {} return false; } } /** * 劫持 window.open * 拦截通过 JS 调用 window.open 打开重定向链接的行为 */ function rewriteWindowOpen(app) { if (typeof unsafeWindow === 'undefined' || typeof unsafeWindow.open !== 'function') return; const originalOpen = unsafeWindow.open; unsafeWindow.open = function (url, target, features) { if (typeof url === 'string') { if (SUSPICIOUS_PARAM_REGEX.test(url) || SUSPICIOUS_PATH_REGEX.test(url) || isShortenerDomain(url)) { try { const a = document.createElement('a'); a.href = url; const { url: resolved } = app.resolver.resolveSync(a); if (resolved && resolved !== url && isValidUrl(resolved)) { url = resolved; } } catch (e) {} } } return originalOpen.call(this, url, target, features); }; } // ========================================== // 缓存管理 UI 样式与逻辑 // ========================================== const UI_CSS = ` .mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0); backdrop-filter: blur(0px); z-index: 2147483646; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; pointer-events: auto; animation: fade-in 0.3s forwards; } .panel { background: #fff; border-radius: 24px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); padding: 24px; display: flex; flex-direction: column; gap: 12px; width: 90%; max-width: 480px; font-family: system-ui, -apple-system, sans-serif; box-sizing: border-box; position: relative; transform: scale(0.9); opacity: 0; animation: scale-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; max-height: 90vh; overflow: hidden; } .title { margin: 0 0 5px 0; font-size: 16px; font-weight: 700; color: #1a1a1a; text-align: center; } .subtitle { font-size: 11px; color: #888; text-align: center; margin-bottom: 8px; } .tabs { display: flex; gap: 4px; margin-bottom: 10px; } .tab { flex: 1; padding: 8px; border: none; border-radius: 8px; background: #f0f2f5; font-size: 11px; cursor: pointer; } .tab.active { background: #007AFF; color: #fff; } .content { flex: 1; overflow-y: auto; min-height: 100px; max-height: 250px; } .btn-group { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } button { border: none; border-radius: 12px; padding: 10px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s; background: #f0f2f5; color: #444; display: flex; align-items: center; justify-content: center; } button:hover { background: #e4e6e9; transform: translateY(-1px); } button:active { transform: scale(0.95); } button.primary { background: #007AFF; color: #fff; } button.primary:hover { background: #0063cc; box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3); } button.danger { background: #ff4d4f; color: #fff; } button.danger:hover { background: #d9363e; box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3); } .list-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 11px; } .list-item:last-child { border-bottom: none; } .list-item span { word-break: break-all; max-width: 70%; } .footer { margin-top: 10px; display: flex; gap: 8px; } .stats { font-size: 10px; color: #999; text-align: center; padding: 5px 0; } @media (prefers-color-scheme: dark) { .panel { background: #1c1c1e; color: #fff; } .title { color: #fff; } .subtitle { color: #aaa; } button { background: #2c2c2e; color: #ccc; } button:hover { background: #3a3a3c; } .tab { background: #2c2c2e; color: #ccc; } .tab.active { background: #007AFF; color: #fff; } .list-item { border-bottom-color: #333; } } @keyframes fade-in { to { background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(8px); } } @keyframes scale-in { to { transform: scale(1); opacity: 1; } } `; let shadowRoot = null; let currentTab = 'domain'; // 确保隔离的 Shadow DOM 环境已创建 function ensureShadow() { if (shadowRoot) return; const container = document.createElement('div'); container.id = 'redirect-cache-manager-container'; container.style.cssText = 'position:absolute;top:0;left:0;z-index:2147483647;'; document.documentElement.appendChild(container); shadowRoot = container.attachShadow({ mode: 'closed' }); const style = document.createElement('style'); style.textContent = UI_CSS; shadowRoot.appendChild(style); } // 渲染域名映射列表 function renderDomainTab() { const map = domainResolverMap.get(); const domainList = Object.keys(map).sort(); const items = domainList.length ? domainList .map(domain => `
${domain}
${map[domain]}
`) .join('') : '
暂无缓存域名
'; return `
域名到解析器的映射
${items}
`; } // 渲染 URL 缓存列表 function renderUrlTab() { const stats = urlCache.getStats(); const cache = Object.fromEntries(urlCache.memCache); const urlList = Object.keys(cache).sort().slice(0, 100); // 限制展示数量 const items = urlList.length ? urlList .map(url => { const item = cache[url]; const isExpired = Date.now() - item.timestamp >= CACHE_TTL; const style = isExpired ? 'color:#999;text-decoration:line-through;' : ''; const shortUrl = url.length > 40 ? url.slice(0, 37) + '...' : url; const timeLabel = isExpired ? '已过期' : new Date(item.timestamp).toLocaleTimeString(); return `
${shortUrl}
${timeLabel}
`; }) .join('') : '
暂无缓存链接
'; return `
URL 解析缓存 (最多显示 100 条)
总计: ${stats.total} | 有效: ${stats.valid} | 过期: ${stats.expired}
${items}
`; } // 显示缓存管理弹窗 function showCacheManager() { ensureShadow(); const oldMask = shadowRoot.querySelector('.mask'); if (oldMask) oldMask.remove(); const mask = document.createElement('div'); mask.className = 'mask'; const initialContent = currentTab === 'domain' ? renderDomainTab() : renderUrlTab(); mask.innerHTML = `
缓存管理
${initialContent}
`; // 统一绑定事件处理函数 function bindEvents() { mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); }); mask.querySelectorAll('.tab').forEach(btn => { btn.onclick = () => { currentTab = btn.dataset.tab; mask.querySelectorAll('.tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); mask.querySelector('#tabContent').innerHTML = currentTab === 'domain' ? renderDomainTab() : renderUrlTab(); bindEvents(); }; }); mask.querySelector('#closeBtn').addEventListener('click', () => mask.remove()); mask.querySelectorAll('button[data-domain]').forEach(btn => { btn.onclick = e => { e.stopPropagation(); domainResolverMap.delete(btn.dataset.domain); mask.querySelector('#tabContent').innerHTML = renderDomainTab(); bindEvents(); }; }); mask.querySelectorAll('button[data-url]').forEach(btn => { btn.onclick = e => { e.stopPropagation(); urlCache.delete(btn.dataset.url); mask.querySelector('#tabContent').innerHTML = renderUrlTab(); bindEvents(); }; }); mask.querySelector('#clearDomainBtn')?.addEventListener('click', () => { if (confirm('确认清空所有域名映射?')) { domainResolverMap.clearAll(); mask.querySelector('#tabContent').innerHTML = renderDomainTab(); bindEvents(); } }); mask.querySelector('#clearUrlBtn')?.addEventListener('click', () => { if (confirm('确认清空所有链接缓存?')) { urlCache.clearAll(); mask.querySelector('#tabContent').innerHTML = renderUrlTab(); bindEvents(); } }); } bindEvents(); shadowRoot.appendChild(mask); } // ========================================== // 脚本入口初始化 // ========================================== (async () => { // 1. 优先检查当前页面是否为重定向中间页,如果是则直接跳走 const autoJump = new AutoJumpApp(); if (await autoJump.bootstrap()) { console.log('Auto-jump executed'); return; } // 2. 初始化主重定向解析应用 const redirectApp = new RedirectApp(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => redirectApp.start(), { once: true }); } else { redirectApp.start(); } // 3. 劫持 window.open rewriteWindowOpen(redirectApp); // 4. 注册油猴菜单命令 GM_registerMenuCommand('缓存管理', showCacheManager); GM_registerMenuCommand('清除过期缓存', () => { urlCache.clearExpired(); urlCache.flush(); alert('已清除过期缓存'); }); GM_registerMenuCommand('清除所有缓存', () => { if (confirm('确认清除所有缓存和统计数据?')) { urlCache.clearAll(); domainResolverMap.clearAll(); resolverStats.clearAll(); alert('已清除所有数据'); } }); GM_registerMenuCommand('切换点击拦截', () => { CONFIG.enableClickIntercept = !CONFIG.enableClickIntercept; GM_setValue('redirectConfig', CONFIG); alert('点击拦截已' + (CONFIG.enableClickIntercept ? '开启' : '关闭') + '\n刷新页面生效'); }); GM_registerMenuCommand('切换跟踪清理', () => { CONFIG.enableTrackingCleanup = !CONFIG.enableTrackingCleanup; GM_setValue('redirectConfig', CONFIG); alert('跟踪清理已' + (CONFIG.enableTrackingCleanup ? '开启' : '关闭') + '\n刷新页面生效'); }); })(); })();