// ==UserScript== // @name URLCleaner - 通用链接净化 // @namespace You Boy // @version 1.0 // @description 自动净化链接,移除烦人的追踪参数,让您的网络足迹更干净、隐私更安全。性能至上,静默运行,对网页零侵入。支持灵活的自定义规则,是您掌控链接、保护隐私的终极利器。 // @author You Boy // @match *://*/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; if (window.self !== window.top) { return; } // --- 沙箱环境 --- const Sandbox = { DEFAULT_CONFIG: { general: { params: ['spm_id_from', 'from_source', 'utm_source'] }, rules: [] }, config: null, // 加载配置 loadConfig() { let config = GM_getValue('ulcConfig'); if (!config || typeof config.general === 'undefined' || typeof config.rules === 'undefined') { config = JSON.parse(JSON.stringify(this.DEFAULT_CONFIG)); } config.general = config.general || { params: [] }; config.rules = config.rules || []; config.rules.forEach(rule => { if (typeof rule.match === 'string') { rule.match = [rule.match]; } }); this.config = config; }, // 初始化事件和菜单命令 init() { // 监听来自注入代码的保存请求 window.addEventListener('ulc-save-config', (event) => { GM_setValue('ulcConfig', event.detail); }); // 注册油猴菜单 GM_registerMenuCommand('设置', () => { window.dispatchEvent(new CustomEvent('ulc-open-settings')); }); } }; const StyleInjector = { inject() { GM_addStyle(` #ulc-settings-panel { all: initial; display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2147483647; width: 90vw; min-width: 600px; max-width: 800px; height: 500px; max-height: 80vh; background: #fff; border-radius: 8px; box-shadow: 0 8px 20px rgba(0,0,0,0.2); display: flex; flex-direction: row; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; color: #333; font-size: 14px; } #ulc-settings-panel *, #ulc-settings-panel *::before, #ulc-settings-panel *::after { box-sizing: border-box; margin: 0; padding: 0; border: 0; font: inherit; vertical-align: baseline; background: transparent; color: inherit; text-align: left; line-height: 1.5; } #ulc-settings-panel div, #ulc-settings-panel span, #ulc-settings-panel ul, #ulc-settings-panel li, #ulc-settings-panel label { all: unset; box-sizing: border-box; } #ulc-settings-panel h3 { all: unset; box-sizing: border-box; display: block; font-size: 16px; font-weight: 600; } #ulc-settings-panel button { all: unset; box-sizing: border-box; display: inline-block; text-align: center; cursor: pointer; border-radius: 4px; padding: 8px 15px; font-size: 14px; transition: background-color 0.2s, color 0.2s; line-height: 1; white-space: nowrap; } #ulc-settings-panel input, #ulc-settings-panel textarea { all: unset; box-sizing: border-box; display: block; width: 100%; border: 1px solid #ccc; border-radius: 4px; padding: 10px; font-size: 14px; margin-bottom: 15px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #fff; line-height: 1.4; } #ulc-settings-panel input[type="checkbox"] { all: unset; box-sizing: border-box; appearance: none; -webkit-appearance: none; display: inline-block; width: 16px; height: 16px; border: 1px solid #ccc; border-radius: 4px; margin-right: 8px; vertical-align: middle; position: relative; cursor: pointer; flex-shrink: 0; } #ulc-settings-panel textarea { min-height: 80px; resize: vertical; } #ulc-settings-panel textarea::placeholder { white-space: pre-wrap; word-wrap: break-word; } #ulc-settings-panel code { width: initial; height: initial; display: initial; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; } #ulc-settings-panel button.ulc-btn-primary { background-color: #00a1d6; color: #fff; padding-block: 12px; } #ulc-settings-panel button.ulc-btn-primary:hover { background-color: #00b5e5; } #ulc-settings-panel button.ulc-btn-secondary { background-color: #fff; color: #767676; border: 1px solid #e3e3e3; } #ulc-settings-panel button.ulc-btn-secondary:hover { background-color: #e0e0e0; } #ulc-settings-panel button.ulc-btn-danger { border: 1px solid #ff4d4d; color: #ff4d4d; } #ulc-settings-panel button.ulc-btn-danger:hover { background-color: #ff4d4d; color: white; } #ulc-settings-panel .ulc-sidebar { display:flex; width: 180px; border-right: 1px solid #eee; flex-shrink: 0; flex-direction: column; } #ulc-settings-panel .ulc-search-container { padding: 10px 15px 0; } #ulc-settings-panel #ulc-rule-search { all: unset; box-sizing: border-box; width: 100%; border: 1px solid #ddd; border-radius: 4px; padding: 6px 10px; font-size: 13px; background-color: #fff; } #ulc-settings-panel .ulc-tabs { display: block; list-style: none; padding: 13px 0 10px; flex-grow: 1; overflow-y: auto; } #ulc-settings-panel .ulc-tab { position: relative; display: flex; align-items: center; height: 40px; padding: 0 18px; cursor: pointer; contain: strict; content-visibility: auto; contain-intrinsic-size: auto 40px; min-width: 0; } #ulc-settings-panel .ulc-tab::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 50%; background-color: transparent; transition: background-color 0.2s; } #ulc-settings-panel .ulc-tab:hover { background: #f5f5f5; } #ulc-settings-panel .ulc-tab.active { font-weight: 600; color: #00a1d6; } #ulc-settings-panel .ulc-tab.active::before { background-color: #00a1d6; } #ulc-settings-panel .ulc-tab > span { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #ulc-settings-panel #ulc-add-rule-btn { display: block; text-align: center; padding: 12px; cursor: pointer; background: #fafafa; border-top: 1px solid #eee; color: #333; font-size: 14px; flex-shrink: 0; } #ulc-settings-panel #ulc-add-rule-btn::after { content: ' 新增规则'; } #ulc-settings-panel #ulc-add-rule-btn:hover { background: #f0f0f0; } #ulc-settings-panel .ulc-main-content { display:flex; flex-grow: 1; flex-direction: column; overflow: hidden; } #ulc-settings-panel .ulc-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 15px; min-height: 55px; border-bottom: 1px solid #eee; flex-shrink: 0; } #ulc-settings-panel .ulc-title-container { display: flex; align-items: center; flex-grow: 1; max-width: 80%; } #ulc-settings-panel .ulc-title-container > h3 { max-width: 80%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #ulc-settings-panel .ulc-edit-icon { display: none; cursor: pointer; margin-left: 8px; width: 16px; height: 16px; vertical-align: middle; } #ulc-settings-panel .ulc-title-container:hover .ulc-edit-icon { display: inline-block; } #ulc-settings-panel #ulc-close-btn { font-size: 24px; cursor: pointer; color: #999; padding: 5px; line-height: 1; flex-shrink: 0; } #ulc-settings-panel #ulc-close-btn:hover { color: #333; } #ulc-settings-panel .ulc-sub-header { display: flex; align-items: flex-start; justify-content: flex-start; padding: 8px 15px; background: #f9f9f9; border-bottom: 1px solid #eee; font-size: 12px; color: #666; flex-shrink: 0; } #ulc-settings-panel .ulc-sub-header > span { display: inline; flex-shrink: 0; margin-right: 8px; line-height: 22px; } #ulc-settings-panel .ulc-match-tags { display: flex; flex-wrap: wrap; gap: 6px; max-height: 26px; overflow: hidden; transition: max-height 0.3s ease; flex-grow: 1; } #ulc-settings-panel .ulc-match-tags:hover { max-height: 200px; } #ulc-settings-panel .ulc-match-tags code { display: inline; background: #e9e9e9; color: #c7254e; padding: 2px 6px; border-radius: 4px; font-size: 12px; white-space: nowrap; } #ulc-settings-panel .ulc-add { display: flex; align-items: center; padding: 10px 15px; border-bottom: 1px solid #eee; flex-shrink: 0; } #ulc-settings-panel #ulc-new-param { margin-right: 10px; padding: 8px; margin-bottom: 0; } #ulc-settings-panel .ulc-list { display: flex; padding: 10px; overflow-y: auto; flex-grow: 1; flex-wrap: wrap; align-content: flex-start; } #ulc-settings-panel .ulc-list:empty::before { content: "未添加参数"; display: block; width: 100%; text-align: center; color: #999; font-size: 14px; padding: 20px; } #ulc-settings-panel .ulc-list-transform { position: relative; max-height: 100px; } #ulc-settings-panel .ulc-list-transform::before { content:"跳转参数"; display: inline-block; background: #FFF; position: absolute; top: -10px; left: 10px; padding: 0 5px; font-size: 12px; color: #999; } #ulc-settings-panel .ulc-list-transform .ulc-list-transform-content { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px; border-top: 1px solid #eee; flex-shrink: 0; overflow-y: auto; height: 100%; } #ulc-settings-panel .ulc-list-transform .ulc-list-transform-content > span { display: inline-block; background: #fceeee; color: #333; padding: 3px 6px; border-radius: 3px; margin: 0; font-size: 14px; } #ulc-settings-panel .ulc-param { display: inline-flex; align-items: center; background: #eef0f2; color: #333; padding: 5px 10px; border-radius: 6px; margin: 5px; font-size: 14px; } #ulc-settings-panel .ulc-param span { display: inline; margin-right: 8px; } #ulc-settings-panel .ulc-delete { color: #999; cursor: pointer; font-weight: bold; font-size: 16px; line-height: 1; padding: 4px 8px; margin: -4px -8px; border-radius: 6px; } #ulc-settings-panel .ulc-delete:hover { color: #ff4d4d; } #ulc-settings-panel .ulc-rule-settings-footer { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-top: 1px solid #eee; font-size: 13px; color: #555; flex-shrink: 0; } #ulc-settings-panel .ulc-rule-settings-footer label { display: flex; align-items: center; cursor: pointer; } #ulc-settings-panel .ulc-rule-settings-footer input[type="checkbox"]:checked { background-color: #00a1d6; border-color: #00a1d6; } #ulc-settings-panel .ulc-rule-settings-footer input[type="checkbox"]:checked::after { all: unset; box-sizing: border-box; content: ''; display: block; width: 5px; height: 9px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); position: absolute; left: 5px; top: 2px; } #ulc-settings-panel .ulc-rule-settings-footer #ulc-config-text-btn { border-style: dashed; } #ulc-settings-panel .ulc-form-content { display: block; padding: 8px; flex-grow: 1; overflow-y: auto; } #ulc-settings-panel .ulc-form-content label { display: block; margin-bottom: 8px; font-weight: 500; margin-top: 3em; } #ulc-settings-panel .ulc-form-content label:first-child { margin-top: 0; } #ulc-settings-panel .ulc-form-content p { font-size: 12px; display: block; color: #999; } #ulc-settings-panel .ulc-form-actions { display: flex; padding: 15px; border-top: 1px solid #eee; justify-content: flex-end; gap: 10px; flex-shrink: 0; } #ulc-settings-panel .ulc-form-hint { display: block; font-size: 12px; color: #666; margin-top: -5px; margin-bottom: 15px; } #ulc-settings-panel .ulc-hint-title { display: block; font-weight: bold; margin-top: 8px; } #ulc-settings-panel .ulc-hint-line { display: flex; align-items: center; margin-top: 4px; } #ulc-settings-panel .ulc-hint-line code { display: inline-block; flex-shrink: 0; background: #f5f5f5; color: #c7254e; padding: 2px 6px; border-radius: 4px; font-size: 12px; } #ulc-settings-panel .ulc-hint-line span { display: inline; margin-left: 8px; } #ulc-settings-panel #ulc-config-textarea { height: 100%; min-height: 100px; resize: vertical; margin-bottom: 0; } #ulc-settings-panel #ulc-toast { all: initial; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; position: absolute; top: 60px; left: calc( 50% + 90px ); transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.75); color: white; padding: 10px 20px; border-radius: 20px; font-size: 14px; z-index: 10; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; white-space: pre-wrap; line-height: 1.4; display: inline-block; max-width: 560px; } #ulc-settings-panel #ulc-toast.show { opacity: 1; visibility: visible; } @media (max-width: 600px) { #ulc-settings-panel { width: 100vw; height: 100vh; max-height: 100vh; min-width: 0; border-radius: 0; flex-direction: column; } #ulc-settings-panel .ulc-sidebar { width: 100%; height: auto; flex-direction: row; flex-wrap: wrap; align-items: center; border-right: 0; border-bottom: 1px solid #eee; flex-shrink: 0; padding: 12px 12px 0; gap: 10px; } #ulc-settings-panel .ulc-search-container { order: 1; flex-grow: 1; padding: 0; } #ulc-settings-panel #ulc-rule-search { font-size: 15px; padding: 10px 12px; } #ulc-settings-panel #ulc-add-rule-btn { order: 2; flex-shrink: 0; padding: 0; margin: 0; border: 1px solid #ddd; font-size: 0; line-height: 1; background: #fff; position: relative; display: flex; justify-content: center; align-items: center; height: 39px; width: 39px; } #ulc-settings-panel #ulc-add-rule-btn::after { font-size: 20px; content: '+'; } #ulc-settings-panel .ulc-tabs { order: 3; flex-basis: 100%; height: auto; display: flex; flex-direction: row; overflow-x: auto; white-space: nowrap; padding: 0; margin-top: 10px; border-top: 1px solid #f0f0f0; } #ulc-settings-panel .ulc-tab { display: flex; justify-content: center; align-items: center; width: 90px; height: 40px; padding: 0 10px; border-left: 0; border-bottom: 3px solid transparent; font-size: 15px; flex-shrink: 0; contain: strict; content-visibility: auto; contain-intrinsic-size: 90px 40px; min-width: 0; } #ulc-settings-panel .ulc-tab::before { display: none; } #ulc-settings-panel .ulc-tab.active { border-bottom-color: #00a1d6; color: #00a1d6; background-color: transparent; } #ulc-settings-panel .ulc-param { padding: 8px 12px; font-size: 15px; } #ulc-settings-panel .ulc-delete { padding: 8px; } #ulc-settings-panel .ulc-edit-icon { display:inline-block; } #ulc-settings-panel #ulc-toast { left: 50%; } } `); } }; const CodeInjector = { injectedCode: function () { (() => { console.log('injected code running'); // --- State (状态管理) --- const State = { config: null, DEFAULT_CONFIG: null, paramsToRemove: new Set(), transformKeysToUse: new Set(), cleanedAttrName: '', invalidAttrName: '', ui: { activeTab: 'general', activeRuleIndex: -1, view: 'list', // 'list', 'add', 'edit', 'config-text' searchQuery: '' }, dom: { settingsPanel: null, sidebarContainer: null, mainContentContainer: null, }, toastTimer: null, init(config, defaultConfig) { this.config = config; this.DEFAULT_CONFIG = defaultConfig; }, }; // --- Core (核心净化与转换逻辑) --- const Core = { saveConfig() { window.dispatchEvent(new CustomEvent('ulc-save-config', { detail: State.config })); this.setActiveParameters(); }, // 字符串转为正则表达式对象 wildcardToRegex(pattern) { try { if (pattern.startsWith('re:')) { return new RegExp(pattern.substring(3)); } // 将 pattern 分解为协议、主机和路径部分 let protocol = '*'; let host = pattern; let path = '/*'; if (host.includes('://')) { const parts = host.split('://'); protocol = parts[0]; host = parts[1]; } if (host.includes('/')) { const hostParts = host.split('/'); host = hostParts.shift(); path = '/' + hostParts.join('/'); if (!path.endsWith('*')) { path += '*'; } } // 对每个部分进行转义和通配符替换 const protocolRegex = protocol.replace(/\*/g, 'https?'); const hostRegex = host.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*'); const pathRegex = path.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); const finalRegexString = `^${protocolRegex}://${hostRegex}${pathRegex}$`; return new RegExp(finalRegexString); } catch (e) { // 确保在转换失败时不报错 return new RegExp('$.'); } }, setActiveParameters() { const matchingRules = []; for (const rule of State.config.rules) { for (const match of rule.match) { if (this.wildcardToRegex(match).test(window.location.href)) { matchingRules.push(rule); break; } } } const params = new Set(); const transforms = new Set(); if (matchingRules.length > 0) { let shouldApplyGeneral = false; matchingRules.forEach(rule => { (rule.params || []).forEach(p => params.add(p)); if (Array.isArray(rule.transform)) { rule.transform.forEach(t => transforms.add(t)); } if (rule.applyGeneral) { shouldApplyGeneral = true; } }); if (shouldApplyGeneral) { (State.config.general.params || []).forEach(p => params.add(p)); } } else { (State.config.general.params || []).forEach(p => params.add(p)); } State.paramsToRemove = params; State.transformKeysToUse = transforms; }, // 严格检查是否是有效的URL isValidAbsoluteURL(str) { if (typeof str !== 'string' || str.trim() === '') return false; try { const url = new URL(str); return ['http:', 'https:', 'ftp:', 'ftps:'].includes(url.protocol); } catch (e) { return false; } }, // 宽松地尝试解析URL tryParseURL(str) { if (typeof str !== 'string' || str.trim() === '') return null; try { // 必须看起来像URL才尝试,避免 "a,b,c" 被解析 if (str.includes('://') || str.startsWith('/') || str.startsWith('?') || str.startsWith('#')) { return new URL(str, window.location.href); } return null; } catch (e) { return null; } }, // 尝试所有解码方式 tryAllDecodes(value) { if (!value) return null; // 解码函数 const decoders = [ (val) => atob(val), (val) => decodeURIComponent(val), (val) => decodeURIComponent(decodeURIComponent(val)), ]; const applyDecoders = (input) => { if (this.isValidAbsoluteURL(input)) return input; for (const decoder of decoders) { try { const decoded = decoder(input); if (decoded && this.isValidAbsoluteURL(decoded)) { return decoded; } } catch (error) { } } return null; }; const variants = [ value, // 原始值 value.split('').reverse().join(''), // 字符串反转 ]; for (const variant of variants) { const decoded = applyDecoders(variant); if (decoded) return decoded; } return null; }, // 从奇怪的参数中提取URL extractUrlFromWeirdParam(input) { try { const url = input instanceof URL ? input : new URL(input); const [key, value] = url.searchParams.entries().next().value || []; if (url.searchParams.size === 1 && !value) { const decoded = this.tryAllDecodes(key); if (decoded) return decoded; } } catch (_) { } return null; }, cleanUrl(urlString, recursionDepth = 0) { // 增加熔断机制防止无限递归 const MAX_RECURSION_DEPTH = 3; if (recursionDepth > MAX_RECURSION_DEPTH) { return urlString; } if (!urlString || typeof urlString !== 'string') return urlString; const originalUrlString = urlString; const isOriginalRelative = !/^(https?:)?\/\//.test(originalUrlString); let currentUrl; try { currentUrl = new URL(originalUrlString, window.location.href); } catch (e) { return originalUrlString; } // --- 1:链接转换 --- if (State.transformKeysToUse.size > 0) { if (currentUrl.searchParams.size === 1) { const weirdUrl = this.extractUrlFromWeirdParam(currentUrl.href); if (weirdUrl) return weirdUrl; } for (const key of currentUrl.searchParams.keys()) { if (State.transformKeysToUse.has(key)) { const value = currentUrl.searchParams.get(key); const transformedUrl = this.tryAllDecodes(value); if (transformedUrl) { return this.cleanUrl(transformedUrl, recursionDepth + 1); } } } } // --- 2:参数净化 --- const finalUrlObject = new URL(currentUrl.href); let modified = false; // 创建一个临时的Set来检查,避免重复遍历 const paramsToCheck = new Set(finalUrlObject.searchParams.keys()); if (paramsToCheck.size > 0) { for (const param of State.paramsToRemove) { if (paramsToCheck.has(param)) { finalUrlObject.searchParams.delete(param); modified = true; } } } if (!modified) return originalUrlString; if (isOriginalRelative) { return finalUrlObject.pathname + finalUrlObject.search + finalUrlObject.hash; } else { return finalUrlObject.href; } }, isValidHttpLink(linkElement) { if (!linkElement || linkElement.tagName !== 'A') return false; const hrefAttr = linkElement.getAttribute('href'); if (!hrefAttr || hrefAttr.trim().startsWith('#') || hrefAttr.trim().startsWith('javascript:')) return false; try { const url = new URL(linkElement.href); return ['http:', 'https:'].includes(url.protocol); } catch (error) { return false; } }, }; // --- UI (界面渲染) --- const UI = { setSafelyInnerHTML(element, htmlString) { if (window.trustedTypes && window.trustedTypes.createPolicy) { try { const policy = window.trustedTypes.createPolicy('safe-html-setter#3', { createHTML: string => string }); element.innerHTML = policy.createHTML(htmlString); } catch (e) { element.innerHTML = htmlString; } } else { element.innerHTML = htmlString; } }, showToast(message, duration = 2000) { const toast = document.getElementById('ulc-toast'); if (!toast) return; toast.textContent = message; toast.classList.add('show'); if (State.toastTimer) clearTimeout(State.toastTimer); State.toastTimer = setTimeout(() => { toast.classList.remove('show'); State.toastTimer = null; }, duration); }, createSettingsPanel() { if (State.dom.settingsPanel) return; const panel = document.createElement('div'); panel.id = 'ulc-settings-panel'; this.setSafelyInnerHTML(panel, `
`); document.body.appendChild(panel); State.dom.settingsPanel = panel; State.dom.sidebarContainer = panel.querySelector('.ulc-sidebar'); State.dom.mainContentContainer = panel.querySelector('.ulc-main-content'); panel.addEventListener('click', Events.handlePanelClick); panel.addEventListener('keydown', e => { if (e.key === 'Enter' && e.target.id === 'ulc-new-param') Events.addParamsFromInput(); }); }, renderPanel() { if (!State.dom.settingsPanel) return; this.renderSidebar(); this.renderMainContent(); const input = document.getElementById('ulc-new-param') || document.getElementById('ulc-rule-name'); if (input) input.focus(); }, updateRuleList() { const tabsContainer = State.dom.sidebarContainer.querySelector('.ulc-tabs'); if (!tabsContainer) return; const searchQuery = (State.ui.searchQuery || '').toLowerCase(); const fragment = document.createDocumentFragment(); const generalTab = document.createElement('li'); generalTab.className = 'ulc-tab'; generalTab.dataset.tabId = 'general'; generalTab.dataset.ruleIndex = '-1'; generalTab.textContent = '通用规则'; if (State.ui.activeTab === 'general') { generalTab.classList.add('active'); } fragment.appendChild(generalTab); State.config.rules.forEach((rule, index) => { const li = document.createElement('li'); li.className = 'ulc-tab'; li.dataset.tabId = `rule-${index}`; li.dataset.ruleIndex = index.toString(); li.title = `${rule.name}\n${rule.match.join('\n')}`; const textSpan = document.createElement('span'); textSpan.textContent = rule.name; li.appendChild(textSpan); const isVisible = searchQuery ? rule.name.toLowerCase().includes(searchQuery) : true; if (!isVisible) { li.style.display = 'none'; } if (State.ui.activeTab === `rule-${index}`) { li.classList.add('active'); } fragment.appendChild(li); }); tabsContainer.innerHTML = ''; tabsContainer.appendChild(fragment); const activeTabEl = tabsContainer.querySelector('.ulc-tab.active'); if (activeTabEl) { requestAnimationFrame(() => activeTabEl.scrollIntoView({ block: 'nearest', behavior: 'auto' })); } }, renderSidebar() { if (!State.dom.sidebarContainer) return; const sidebarHtml = `
+
`; this.setSafelyInnerHTML(State.dom.sidebarContainer, sidebarHtml); // 绑定事件到稳定的搜索框 const searchInput = State.dom.sidebarContainer.querySelector('#ulc-rule-search'); if (searchInput) { searchInput.value = State.ui.searchQuery || ''; if (Events.onSearchInputDebounced) { searchInput.addEventListener('input', Events.onSearchInputDebounced); } const isMobile = window.innerWidth <= 600; if (!isMobile && document.activeElement !== searchInput) { searchInput.focus(); searchInput.selectionStart = searchInput.selectionEnd = searchInput.value.length; } } // 列表渲染 this.updateRuleList(); }, renderMainContent() { if (!State.dom.mainContentContainer) return; let contentHtml = ''; if (State.ui.view === 'list') contentHtml = this.renderRuleDetails(); else if (State.ui.view === 'add' || State.ui.view === 'edit') contentHtml = this.renderRuleForm(); else if (State.ui.view === 'config-text') contentHtml = this.renderConfigTextForm(); this.setSafelyInnerHTML(State.dom.mainContentContainer, contentHtml); const input = document.getElementById('ulc-new-param') || document.getElementById('ulc-rule-name') || document.getElementById('ulc-config-textarea'); if (input) input.focus(); }, renderRuleDetails() { const isGeneral = State.ui.activeTab === 'general'; const rule = !isGeneral ? State.config.rules[State.ui.activeRuleIndex] : null; if (!isGeneral && !rule) { State.ui.activeTab = 'general'; State.ui.activeRuleIndex = -1; return this.renderRuleDetails(); } const params = isGeneral ? State.config.general.params : (rule.params || []); const transform = isGeneral ? [] : (rule.transform || []); const title = isGeneral ? '通用参数列表' : rule.name; const editIcon = !isGeneral ? `` : ''; const subHeader = !isGeneral ? `
匹配地址:
${rule.match.map(m => `${m}`).join('')}
` : ''; const footerHtml = isGeneral ? ` ` : ` `; return `

${title}

${editIcon}
${subHeader}
${(params).sort().map(p => `
${p}
×
`).join('')}
${transform.length > 0 ? `
${transform.map(t => `${t}`).join('')}
` : ''} ${footerHtml}`; }, renderRuleForm() { const isEdit = State.ui.view === 'edit'; const rule = isEdit ? State.config.rules[State.ui.activeRuleIndex] : null; const title = isEdit ? '编辑净化规则' : '新增净化规则'; let ruleName = '', matchPatterns = '', transformKeys = ''; if (isEdit && rule) { ruleName = rule.name; matchPatterns = rule.match.join('\n'); transformKeys = Array.isArray(rule.transform) ? rule.transform.join('\n') : ''; } else { try { const hostname = window.location.hostname; if (hostname && hostname !== 'localhost') { const parts = hostname.split('.').filter(p => p); ruleName = parts.length > 1 ? parts.slice(-2).join('.') : hostname; matchPatterns = hostname; } } catch (e) { console.error("Could not get domain", e); } } return `

${title}

常用示例:
www.example.com仅匹配指定子域名 (推荐)
*example.com匹配主域名及其所有子域名
进阶示例:
https://www.youtube.com/watch*匹配特定开头的路径
re:[^/]+\\.example\\.com/path/使用正则表达式

部分网站跳转外链的时候会跳转到一个确认网页,配置参数会把对应参数内的外链直接转换为可点击链接。

`; }, renderConfigTextForm() { const configString = JSON.stringify(State.config, null, 2); return `

配置文本

`; } }; // --- Events (事件处理与数据逻辑) --- const Events = { // --- 防抖函数 --- _debounce(func, delay = 250) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; }, _getCurrentContext() { const isGeneral = State.ui.activeTab === 'general'; if (isGeneral) { return { isGeneral: true, params: State.config.general.params || [], rule: null }; } const rule = State.config.rules[State.ui.activeRuleIndex]; return { isGeneral: false, params: rule ? (rule.params || []) : [], rule: rule }; }, addParamsFromInput() { const input = document.getElementById('ulc-new-param'); if (!input || !input.value) return false; const inputValue = input.value.trim(); let newParams = []; const parsedUrl = Core.tryParseURL(inputValue); if (parsedUrl) { if (parsedUrl.searchParams.size > 0) { newParams = [...parsedUrl.searchParams.keys()]; UI.showToast(`已从链接中提取 ${newParams.length} 个参数`); } else { input.value = ''; return false; } } else { newParams = inputValue.split(',').map(p => p.trim()).filter(p => p); } if (newParams.length === 0) { input.value = ''; return false; } const context = this._getCurrentContext(); if (!context.isGeneral && !context.rule) { return false; } const paramsList = context.params; const paramsSet = new Set(paramsList); let addedCount = 0; newParams.forEach(p => { if (!paramsSet.has(p)) { paramsSet.add(p); addedCount++; } }); if (addedCount === 0) { input.value = ''; return false; } const sortedParams = Array.from(paramsSet).sort(); if (context.isGeneral) { State.config.general.params = sortedParams; } else { if (!context.rule.params) { context.rule.params = []; } context.rule.params = sortedParams; } input.value = ''; return true; }, deleteParam(paramToDelete) { const context = this._getCurrentContext(); const params = context.params; if ((!context.isGeneral && !context.rule) || !params) { return false; } const index = params.indexOf(paramToDelete); if (index > -1) { params.splice(index, 1); return true; } return false; }, saveRule() { const nameInput = document.getElementById('ulc-rule-name'); const matchInput = document.getElementById('ulc-rule-match'); const transformInput = document.getElementById('ulc-transform-keys'); const newName = nameInput.value.trim(); const newMatches = [...new Set(matchInput.value.split('\n').map(m => m.trim()).filter(m => m))]; const newTransformKeys = [...new Set(transformInput.value.split('\n').map(k => k.trim()).filter(k => k))]; if (!newName || newMatches.length === 0) { UI.showToast('规则名称和匹配地址不能为空。'); return false; } const isEdit = State.ui.view === 'edit'; const ruleIndex = isEdit ? State.ui.activeRuleIndex : -1; if (State.config.rules.some((r, i) => r.name.toLowerCase() === newName.toLowerCase() && i !== ruleIndex)) { UI.showToast('错误:已存在同名规则,请使用其他名称。'); return false; } const ruleData = { name: newName, match: newMatches, ...(newTransformKeys.length > 0 && { transform: newTransformKeys }) }; if (isEdit) { const rule = State.config.rules[ruleIndex]; Object.assign(rule, ruleData); if (newTransformKeys.length === 0) delete rule.transform; } else { const newRule = { ...ruleData, params: [], applyGeneral: true }; State.config.rules.push(newRule); State.ui.activeRuleIndex = State.config.rules.length - 1; State.ui.activeTab = `rule-${State.ui.activeRuleIndex}`; } UI.showToast(`规则 "${newName}" 已保存`); return true; }, deleteCurrentRule() { const rule = State.config.rules[State.ui.activeRuleIndex]; if (State.ui.activeTab === 'general' || !rule) return false; if (confirm(`确定要删除规则 "${rule.name}" 吗?`)) { State.config.rules.splice(State.ui.activeRuleIndex, 1); UI.showToast('已删除'); return true; } return false; }, saveConfigFromText() { const textarea = document.getElementById('ulc-config-textarea'); if (!textarea) return false; let newConfig; try { newConfig = JSON.parse(textarea.value); } catch (e) { UI.showToast('JSON 格式无效,请检查您的输入。\n错误信息: ' + e.message, 3000); return false; } // 验证配置文件合法性 const validateConfig = (config) => { if (typeof config !== 'object' || config === null) return "配置必须是一个对象。"; if (typeof config.general !== 'object' || config.general === null) return "配置缺少 'general' 对象。"; if (!Array.isArray(config.general.params)) return "'general.params' 必须是一个数组。"; if (config.general.params.some(p => typeof p !== 'string')) return "'general.params' 数组中包含了非字符串元素。"; if (!Array.isArray(config.rules)) return "配置缺少 'rules' 数组。"; const ruleNames = new Set(); for (let i = 0; i < config.rules.length; i++) { const rule = config.rules[i]; if (typeof rule !== 'object' || rule === null) return `规则 #${i + 1} 不是一个有效的对象。`; if (typeof rule.name !== 'string' || !rule.name.trim()) return `规则 #${i + 1} 缺少有效的 'name' 属性。`; // 检查规则名称是否重复 const ruleName = rule.name.trim().toLowerCase(); if (ruleNames.has(ruleName)) { return `配置中存在重复的规则名称: "${rule.name}"`; } ruleNames.add(ruleName); if (typeof rule.match === 'string') { rule.match = [rule.match]; } if (!Array.isArray(rule.match) || rule.match.length === 0) return `规则 "${rule.name}" 缺少有效的 'match' 数组。`; if (rule.match.some(m => typeof m !== 'string' || !m.trim())) return `规则 "${rule.name}" 的 'match' 数组中包含无效或空元素。`; if (rule.params && !Array.isArray(rule.params)) return `规则 "${rule.name}" 的 'params' 必须是数组。`; if (rule.params && rule.params.some(p => typeof p !== 'string')) return `规则 "${rule.name}" 的 'params' 数组中包含非字符串元素。`; if (rule.transform && !Array.isArray(rule.transform)) return `规则 "${rule.name}" 的 'transform' 必须是数组。`; if (rule.transform && rule.transform.some(t => typeof t !== 'string')) return `规则 "${rule.name}" 的 'transform' 数组中包含非字符串元素。`; } return null; }; const validationError = validateConfig(newConfig); if (validationError) { UI.showToast('配置结构不正确:\n' + validationError, 4000); return false; } State.config.general = newConfig.general; State.config.rules = newConfig.rules; UI.showToast('配置已成功保存'); return true; }, resetConfig() { if (confirm('确定要将通用参数列表重置为默认吗?此操作不可撤销。')) { State.config.general.params = JSON.parse(JSON.stringify(State.DEFAULT_CONFIG.general.params)); UI.showToast('通用参数已重置为默认'); return true; } return false; }, toggleApplyGeneral(isChecked) { if (State.ui.activeTab !== 'general' && State.config.rules[State.ui.activeRuleIndex]) { State.config.rules[State.ui.activeRuleIndex].applyGeneral = isChecked; Core.saveConfig(); } }, onSearchInputDebounced: null, // 防抖处理的 input 事件处理器 _performSearch(query) { if (query !== State.ui.searchQuery) { State.ui.searchQuery = query; UI.updateRuleList(); } }, // 核心事件处理器 handlePanelClick(e) { const target = e.target; const closest = (selector) => target.closest(selector); if (closest('#ulc-close-btn')) { if (State.dom.settingsPanel) { State.dom.settingsPanel.remove(); State.dom.settingsPanel = null; } } else if (closest('.ulc-tab')) { const clickedTab = closest('.ulc-tab'); if (clickedTab.classList.contains('active')) return; State.ui.activeTab = clickedTab.dataset.tabId; State.ui.activeRuleIndex = parseInt(clickedTab.dataset.ruleIndex, 10); State.ui.view = 'list'; const currentActiveTab = State.dom.sidebarContainer.querySelector('.ulc-tab.active'); if (currentActiveTab) currentActiveTab.classList.remove('active'); clickedTab.classList.add('active'); UI.renderMainContent(); } else if (closest('#ulc-add-btn')) { if (this.addParamsFromInput()) { Core.saveConfig(); UI.renderMainContent(); } } else if (closest('.ulc-delete')) { if (this.deleteParam(closest('.ulc-delete').dataset.param)) { Core.saveConfig(); UI.renderMainContent(); } } else if (closest('#ulc-add-rule-btn')) { State.ui.view = 'add'; UI.renderMainContent(); } else if (closest('.ulc-edit-icon')) { State.ui.view = 'edit'; UI.renderMainContent(); } else if (closest('#ulc-delete-rule-btn')) { if (this.deleteCurrentRule()) { State.ui.activeTab = 'general'; State.ui.activeRuleIndex = -1; Core.saveConfig(); UI.renderPanel(); } } else if (closest('#ulc-reset-btn')) { if (this.resetConfig()) { State.ui.activeTab = 'general'; State.ui.activeRuleIndex = -1; Core.saveConfig(); UI.renderMainContent(); } } else if (closest('#ulc-apply-general')) { this.toggleApplyGeneral(target.checked); } else if (closest('#ulc-save-rule-btn')) { if (this.saveRule()) { State.ui.view = 'list'; Core.saveConfig(); UI.renderPanel(); } } else if (closest('#ulc-cancel-add-rule-btn')) { State.ui.view = 'list'; UI.renderMainContent(); } else if (closest('#ulc-config-text-btn')) { State.ui.view = 'config-text'; UI.renderMainContent(); } else if (closest('#ulc-save-config-text-btn')) { if (this.saveConfigFromText()) { State.ui.view = 'list'; State.ui.activeTab = 'general'; State.ui.activeRuleIndex = -1; Core.saveConfig(); UI.renderPanel(); } } else if (closest('#ulc-cancel-config-text-btn')) { State.ui.view = 'list'; UI.renderMainContent(); } }, initEventListeners() { const preCleanLink = (e) => { if (e.target.closest('#ulc-settings-panel')) return; const link = e.target.closest('a[href]'); if (link && !link.dataset[State.cleanedAttrName] && !link.dataset[State.invalidAttrName]) { if (Core.isValidHttpLink(link)) { const cleanedHref = Core.cleanUrl(link.href); if (link.href !== cleanedHref) link.href = cleanedHref; link.dataset[State.cleanedAttrName] = cleanedHref; if (link.hostname !== window.location.hostname) { link.setAttribute('referrerpolicy', 'no-referrer'); } } else { link.dataset[State.invalidAttrName] = 'true'; } } }; document.addEventListener('mouseover', preCleanLink, true); const finalClickFix = e => { const link = e.target.closest('a[href]'); if (link && typeof link.dataset[State.invalidAttrName] === 'undefined') { const cleanedHref = link.dataset[State.cleanedAttrName] || Core.cleanUrl(link.href); if (link.href !== cleanedHref) link.href = cleanedHref; if (link.hostname !== window.location.hostname) { link.setAttribute('referrerpolicy', 'no-referrer'); } e.stopImmediatePropagation(); } }; ['mousedown', 'click', 'contextmenu'].forEach(evt => document.addEventListener(evt, finalClickFix, true)); const wrapHistoryMethod = (method) => { const original = history[method]; history[method] = function (state, title, url, ...rest) { const oldHref = window.location.href; const newCleanedUrl = Core.cleanUrl(url ? url.toString() : ''); const result = original.apply(this, [state, title, newCleanedUrl, ...rest]); requestAnimationFrame(() => { // 重新计算规则 if (window.location.href !== oldHref) { Core.setActiveParameters(); } }); return result; }; }; wrapHistoryMethod('pushState'); wrapHistoryMethod('replaceState'); const originalOpen = window.open; window.open = function (url, target, features) { return originalOpen.apply(this, [Core.cleanUrl(url ? url.toString() : ''), target, features]); }; window.addEventListener('ulc-open-settings', () => { if (State.dom.settingsPanel) { State.dom.settingsPanel.remove(); State.dom.settingsPanel = null; return; } const open = () => { UI.createSettingsPanel(); State.ui.view = 'list'; State.ui.activeTab = 'general'; State.ui.activeRuleIndex = -1; UI.renderPanel(); State.dom.settingsPanel.style.display = 'flex'; }; document.body ? open() : document.addEventListener('DOMContentLoaded', open); }); } }; // --- 初始化 --- function main() { const scriptTag = document.getElementById('ulc-injected-script'); State.init(JSON.parse(scriptTag.dataset.config), JSON.parse(scriptTag.dataset.defaultConfig)); // --- 初始化防抖的搜索处理器 --- Events.onSearchInputDebounced = Events._debounce((e) => { Events._performSearch(e.target.value); }, 250); const { randomString } = (() => { const randomString = () => { const length = Math.floor(Math.random() * 7) + 6; let result = ''; while (result.length < length) result += Math.random().toString(36).substring(2); result = result.substring(0, length); if (/^[0-9]/.test(result)) result = 'p' + result.substring(1); return result; }; return { randomString }; })(); State.cleanedAttrName = randomString(); State.invalidAttrName = randomString(); Core.setActiveParameters(); const cleanedPageUrl = Core.cleanUrl(window.location.href); if (window.location.href !== cleanedPageUrl) { history.replaceState(history.state, '', cleanedPageUrl); } // 绑定所有事件,但 handlePanelClick 需要绑定 Events 对象的上下文 Events.handlePanelClick = Events.handlePanelClick.bind(Events); Events.initEventListeners(); } main(); })(); }, inject(config, defaultConfig) { const nonce = document.querySelector('script[nonce]')?.nonce || document.querySelector('style[nonce]')?.nonce; const finalCodeString = `(${this.injectedCode.toString()})();`; const injectedScript = document.createElement('script'); injectedScript.id = 'ulc-injected-script'; injectedScript.nonce = nonce; injectedScript.dataset.config = JSON.stringify(config); injectedScript.dataset.defaultConfig = JSON.stringify(defaultConfig); if (window.trustedTypes && window.trustedTypes.createPolicy) { try { const policy = window.trustedTypes.createPolicy('UniversalLinkCleanerPolicy', { createScript: s => s }); injectedScript.textContent = policy.createScript(finalCodeString); } catch (e) { injectedScript.textContent = finalCodeString; } } else { injectedScript.textContent = finalCodeString; } (document.head || document.documentElement).appendChild(injectedScript); injectedScript.remove(); } }; // --- 主执行流程 --- function main() { Sandbox.loadConfig(); Sandbox.init(); StyleInjector.inject(); CodeInjector.inject(Sandbox.config, Sandbox.DEFAULT_CONFIG); } main(); })();