${map[domain]}
// ==UserScript== // @name 移动端去除链接重定向 // @author Deepseek // @description 通用的重定向解析 // @version 1.3 // @namespace Violentmonkey Scripts // @grant unsafeWindow // @grant GM.xmlHttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @match *://*/* // @connect * // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // ==================== 配置常量 ==================== const CONFIG = { excludeSameOrigin: false, maxProcessTime: 200, maxResolveDepth: 2, maxConcurrentRequests: 2, requestTimeout: 8000, enableCache: true, cacheExpireDays: 7, maxCacheSize: 500, enableTextExtract: true, enableHtmlEntityDecode: true, enableFallbackResolver: true, excludedProtocols: ['javascript:', 'mailto:', 'tel:', 'sms:', 'data:', 'about:', 'blob:', 'file:', 'ftp:'] }; try { const userConfig = GM_getValue('redirectConfig', {}); Object.assign(CONFIG, userConfig); } 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; // 预编译正则 const SUSPICIOUS_PARAM_REGEX = /[?&](url|u|redirect|goto|to|dest|link|target|out|go|jump)=/i; const SUSPICIOUS_PATH_REGEX = /\/(go|out|redirect|link|to|jump)\//i; const URL_REGEX = /https?:\/\/[^\s"'<>\]\[{}|\\^`]+/gi; const TRACKING_REGEX = /^(spm|from_|ref_|track|trk|share_|embeds_|refer_)|_from$|scm|referrer/i; const BASE64_REGEX = /^[A-Za-z0-9+/=]+$/; const HTML_ENTITY_REGEX = /&[a-z]+;|\d+;/; const URI_ENCODE_REGEX = /%[0-9A-F]{2}/i; // 通用查询参数列表 const COMMON_PARAM_NAMES = [ 'url', 'u', 'target', 'to', 'dest', 'href', 'link', 'goto', 'redirect', 'redirect_url', 'redirect_uri', 'return_url', 'return_to', 'next', 'forward', 'fwd', 'out', 'go', 'jump', 'redirectUrl', 'redirectURI', 'gourl', 'q', 'ru', 'path', 'continue', 'return', 'redirect_to', '_url', 'service_url', 'referer' ]; 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' ]; // ==================== 工具函数 ==================== function isValidUrl(str) { if (!str || typeof str !== 'string') return false; try { new URL(str); return true; } catch { return false; } } function base64Decode(str) { try { let base64 = str.replace(/-/g, '+').replace(/_/g, '/'); while (base64.length % 4) base64 += '='; return atob(base64); } catch (e) { if (str.startsWith('a1')) { try { return base64Decode(str.slice(2)); } catch {} } throw e; } } // 智能解码:根据特征选择解码方式 function smartDecode(str) { if (!str) return str; // URI解码 if (URI_ENCODE_REGEX.test(str)) { try { return decodeURIComponent(str); } catch {} } // Base64解码 if (BASE64_REGEX.test(str) && str.length % 4 === 0) { try { return base64Decode(str); } catch {} } // HTML实体解码 if (HTML_ENTITY_REGEX.test(str)) { try { const textarea = document.createElement('textarea'); textarea.innerHTML = str; return textarea.value; } catch {} } return str; } // 备用解码(兼容旧调用) 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; } function extractUrlFromText(text) { if (!text) return null; const match = text.match(URL_REGEX); return match ? match[0] : null; } function extractDomain(url) { try { return new URL(url).hostname; } catch { return null; } } 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; } } // ==================== 双层缓存管理 ==================== const urlCache = { memCache: new Map(), pendingWrites: new Map(), writeTimer: null, load() { if (!CONFIG.enableCache) return; try { const cache = GM_getValue(URL_CACHE_KEY, {}); for (const [key, value] of Object.entries(cache)) { 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.pendingWrites.delete(originalUrl); this.scheduleWrite(); } return null; }, set(originalUrl, realUrl) { if (!CONFIG.enableCache || !realUrl) return; const item = { url: realUrl, timestamp: Date.now() }; this.memCache.set(originalUrl, item); this.pendingWrites.set(originalUrl, item); this.scheduleWrite(); if (this.memCache.size > MAX_CACHE_SIZE) { const oldestKey = this._findOldestKey(); if (oldestKey) { this.memCache.delete(oldestKey); this.pendingWrites.delete(oldestKey); } } }, scheduleWrite() { if (this.writeTimer) clearTimeout(this.writeTimer); this.writeTimer = setTimeout(() => this.flush(), 5000); }, flush() { if (this.pendingWrites.size === 0) return; try { const cache = GM_getValue(URL_CACHE_KEY, {}); for (const [key, value] of this.pendingWrites) { cache[key] = value; } GM_setValue(URL_CACHE_KEY, cache); this.pendingWrites.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); changed = true; } } if (changed) this.scheduleWrite(); }, clearAll() { this.memCache.clear(); this.pendingWrites.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)) { 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; } async resolve(element) { return null; } isValidUrl(str) { return isValidUrl(str); } } // ==================== 具体解析器 ==================== class AttributeResolver extends RedirectResolver { constructor(extraAttrs = []) { super('AttributeResolver'); this.attributeNames = [ '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', ...extraAttrs ]; } canResolve(element) { if (element.hasAttribute(RedirectApp.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 && this.isValidUrl(value)) { return value; } } return null; } } class QueryParamResolver extends RedirectResolver { constructor(paramNames, decodeTypes = ['uri', 'base64', 'html']) { super('QueryParamResolver'); this.paramNames = Array.isArray(paramNames) ? paramNames : [paramNames]; this.decodeTypes = decodeTypes; // 保留备用 } canResolve(element) { if (element.hasAttribute(RedirectApp.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; } } 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(RedirectApp.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; } } class TextContentResolver extends RedirectResolver { constructor() { super('TextContentResolver'); } canResolve(element) { if (element.hasAttribute(RedirectApp.REDIRECT_COMPLETED)) return false; 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); return url && this.isValidUrl(url) ? url : null; } } 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 match = path.match(/^\/(?:go|out|redirect|link|to|jump)\/(.+)/i); if (match && match[1]) { let target = decodeURIComponent(match[1]); if (!target.startsWith('http')) { target = 'https://' + target; } return this.isValidUrl(target) ? target : null; } } catch {} return null; } } // 增强后备解析器 class EnhancedFallbackResolver extends RedirectResolver { constructor() { super('EnhancedFallbackResolver'); this.requestCache = new Map(); this.pendingRequests = new Map(); this.requestQueue = []; this.maxConcurrent = CONFIG.maxConcurrentRequests; } canResolve(element) { if (!CONFIG.enableFallbackResolver) return false; return !element.hasAttribute(RedirectApp.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; } if (this.pendingRequests.has(href)) { return this.pendingRequests.get(href); } if (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); } return result; } finally { this.pendingRequests.delete(href); if (this.requestQueue.length > 0) { const next = this.requestQueue.shift(); next(); } } } async performRequest(href) { let finalUrl = await this._request('HEAD', href); if (finalUrl && finalUrl !== href) return finalUrl; finalUrl = await this._request('GET', href); return finalUrl || 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,application/xml;q=0.9,*/*;q=0.8', 'User-Agent': navigator.userAgent }, onload: (resp) => { clearTimeout(timeoutId); // 如果已经有 finalUrl 且不同,直接使用 if (resp.finalUrl && resp.finalUrl !== url && isValidUrl(resp.finalUrl)) { resolve(resp.finalUrl); return; } // 处理重定向状态码 if (resp.status >= 300 && resp.status < 400 && resp.responseHeaders) { const locationMatch = resp.responseHeaders.match(/location:\s*([^\r\n]+)/i); if (locationMatch && locationMatch[1]) { let location = locationMatch[1].trim(); try { location = new URL(location, url).href; // 拼接绝对URL if (isValidUrl(location)) { resolve(location); return; } } catch {} } } if (method === 'GET' && 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.failedResolvers = new Map(); } addResolver(resolver) { this.resolvers.push(resolver); } canResolve(element) { return this.resolvers.some(r => r.canResolve(element)); } getResolverByName(name) { return this.resolvers.find(r => r.name === name); } recordFailure(domain, resolverName) { if (!domain || !resolverName) return; if (!this.failedResolvers.has(domain)) { this.failedResolvers.set(domain, new Set()); } this.failedResolvers.get(domain).add(resolverName); } clearFailures(domain) { if (domain) { this.failedResolvers.delete(domain); } } async resolve(element, preferredResolverName = null) { const href = element.href; const domain = extractDomain(href); const failed = domain ? (this.failedResolvers.get(domain) || new Set()) : new Set(); if (preferredResolverName) { const preferred = this.getResolverByName(preferredResolverName); if (preferred && preferred.canResolve(element) && !failed.has(preferred.name)) { 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); this.recordFailure(domain, preferred.name); } } catch (e) { resolverStats.record(domain, preferred.name, false); this.recordFailure(domain, preferred.name); } } } for (const resolver of this.resolvers) { if (resolver.name === preferredResolverName) continue; if (failed.has(resolver.name)) 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); this.recordFailure(domain, resolver.name); } } catch (e) { resolverStats.record(domain, resolver.name, false); this.recordFailure(domain, resolver.name); } } } 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 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; } // ==================== 防抖工具 ==================== function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // ==================== 主应用类 ==================== class RedirectApp { static REDIRECT_COMPLETED = 'data-redirect-completed'; constructor() { this.config = CONFIG; this.resolver = createDefaultResolver(); this.processingElements = new WeakSet(); this.mutationObserver = null; this.styleObserver = null; this.scanDebounce = debounce(this._scanNewLinks.bind(this), 500); } 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; return false; // 不符合特征,跳过处理 } async processLink(element) { if (this.processingElements.has(element)) return; if (element.hasAttribute(RedirectApp.REDIRECT_COMPLETED)) return; if (!this.shouldProcessLink(element)) return; this.processingElements.add(element); try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('处理超时')), this.config.maxProcessTime) ); const cachedUrl = urlCache.get(element.href); if (cachedUrl && cachedUrl !== element.href && isValidUrl(cachedUrl)) { const cleanedUrl = cleanUrlParams(cachedUrl); element.href = cleanedUrl; element.setAttribute(RedirectApp.REDIRECT_COMPLETED, 'true'); return; } const domain = extractDomain(element.href); 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 !== element.href && isValidUrl(realUrl)) { const finalUrl = await this.resolveDeep(realUrl, 0, domain); const cleanedUrl = cleanUrlParams(finalUrl); element.href = cleanedUrl; element.setAttribute(RedirectApp.REDIRECT_COMPLETED, 'true'); urlCache.set(element.href, cleanedUrl); if (domain && resolverName) { domainResolverMap.set(domain, resolverName); this.resolver.clearFailures(domain); } } } catch (error) { console.debug('处理失败:', error); } finally { this.processingElements.delete(element); } } async resolveDeep(url, depth = 0, domain = null) { if (depth >= this.config.maxResolveDepth) return 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)); if (result !== url) { urlCache.set(url, result); } return result; } _scanNewLinks() { document.querySelectorAll(`a:not([${RedirectApp.REDIRECT_COMPLETED}])`) .forEach(link => this.processLink(link)); } 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) { node.querySelectorAll(`a:not([${RedirectApp.REDIRECT_COMPLETED}])`) .forEach(aNode => this.processLink(aNode)); } } } } }; isLinkElement(node) { return node instanceof HTMLAnchorElement || (node.nodeType === 1 && node.tagName === 'A' && node.namespaceURI === 'http://www.w3.org/2000/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() { const links = Array.from(document.querySelectorAll('a')); this.processLinksInBatches(links); this.mutationObserver = new MutationObserver(this.handleMutation); this.mutationObserver.observe(document, { childList: true, subtree: true }); this.styleObserver = new MutationObserver(() => this.scanDebounce()); this.styleObserver.observe(document, { attributes: true, subtree: true, attributeFilter: ['href', 'data-href', 'data-url', 'style', 'class'] }); console.log('🚀 去除重定向优化版启动 | 缓存: ' + (CONFIG.enableCache ? '启用' : '禁用')); } stop() { if (this.mutationObserver) this.mutationObserver.disconnect(); if (this.styleObserver) this.styleObserver.disconnect(); } } // ==================== 自动跳转处理 ==================== class AutoJumpApp { constructor() { this.commonParams = COMMON_PARAM_NAMES; this.maxDepth = CONFIG.maxResolveDepth; } async resolveDeep(url, depth = 0) { if (depth >= this.maxDepth) return 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)) { const final = await this.resolveDeep(decoded, depth + 1); urlCache.set(url, final); return final; } } } catch (e) {} urlCache.set(url, url); return url; } async bootstrap() { try { const currentUrl = location.href; const finalUrl = await this.resolveDeep(currentUrl); if (finalUrl !== currentUrl && isValidUrl(finalUrl)) { const cleaned = cleanUrlParams(finalUrl); console.log(`🔄 自动跳转: ${currentUrl} -> ${cleaned}`); location.href = cleaned; return true; } } catch (e) {} return false; } } // ==================== 拦截 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') { (async () => { try { const a = document.createElement('a'); a.href = url; const { url: resolved } = await app.resolver.resolve(a, null); if (resolved && resolved !== url && isValidUrl(resolved)) { url = resolved; } } catch (e) { } finally { originalOpen.call(this, url, target, features); } })(); return; } return originalOpen.call(this, url, target, features); }; } // ==================== 缓存管理面板 ==================== 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'; 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(); return `