Greasy Fork

Greasy Fork is available in English.

URLCleaner - 通用链接净化

自动净化链接,移除烦人的追踪参数,让您的网络足迹更干净、隐私更安全。性能至上,静默运行,对网页零侵入。支持灵活的自定义规则,是您掌控链接、保护隐私的终极利器。

当前为 2025-07-18 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/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, `
            <div class="ulc-sidebar"></div>
            <div class="ulc-main-content"></div>
            <div id="ulc-toast"></div>
          `);
            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 = `
              <div class="ulc-search-container">
                <input type="search" id="ulc-rule-search" placeholder="搜索规则">
              </div>
              <ul class="ulc-tabs"></ul>
              <div id="ulc-add-rule-btn">+</div>
            `;
            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 ? `<svg class="ulc-edit-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M832 512a32 32 0 1 1 64 0v352a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V160a96 96 0 0 1 96-96h352a32 32 0 0 1 0 64H160a32 32 0 0 0-32 32v704a32 32 0 0 0 32 32h704a32 32 0 0 0 32-32V512zm-101.056-405.504a32 32 0 0 1 45.248 0L904.96 235.264a32 32 0 0 1 0 45.248L583.424 601.984a32 32 0 0 1-18.112 9.088L400 640l28.928-165.312a32 32 0 0 1 9.088-18.112l321.536-321.536zM855.04 256l-45.248-45.248L704.96 315.648l45.248 45.248L855.04 256zm-45.248 45.248L588.224 522.816 542.976 477.568 764.544 256l45.248 45.248z"/></svg>` : '';
            const subHeader = !isGeneral ? `<div class="ulc-sub-header"><span>匹配地址:</span><div class="ulc-match-tags">${rule.match.map(m => `<code>${m}</code>`).join('')}</div></div>` : '';
            const footerHtml = isGeneral ? `
            <div class="ulc-rule-settings-footer">
              <div><button id="ulc-config-text-btn" class="ulc-btn-secondary">配置文本</button></div>
              <button id="ulc-reset-btn" class="ulc-btn-secondary">重置为默认</button>
            </div>` : `
            <div class="ulc-rule-settings-footer">
              <label><input type="checkbox" id="ulc-apply-general" ${rule.applyGeneral ? 'checked' : ''}><span>应用通用规则</span></label>
              <button id="ulc-delete-rule-btn" class="ulc-btn-danger">删除此规则</button>
            </div>`;
            return `
            <div class="ulc-header"><div class="ulc-title-container"><h3 title="${title}">${title}</h3>${editIcon}</div><button id="ulc-close-btn">×</button></div>
            ${subHeader}
            <div class="ulc-add"><input type="text" id="ulc-new-param" placeholder="输入参数,可英文逗号分隔批量添加,或输入一个链接自动提取"/><button id="ulc-add-btn" class="ulc-btn-primary">添加</button></div>
            <div class="ulc-list">${(params).sort().map(p => `<div class="ulc-param"><span>${p}</span><div class="ulc-delete" data-param="${p}">×</div></div>`).join('')}</div>
            ${transform.length > 0 ? `<div class="ulc-list-transform"><div class="ulc-list-transform-content">${transform.map(t => `<span>${t}</span>`).join('')}</div></div>` : ''}
            ${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 `
            <div class="ulc-main-content">
              <div class="ulc-header"><h3>${title}</h3></div>
              <div class="ulc-form-content">
                <label for="ulc-rule-name">规则名称</label>
                <input type="text" id="ulc-rule-name" placeholder="规则名称" maxlength="30" value="${ruleName}">
                <label for="ulc-rule-match">匹配地址 (每行一个)</label>
                <textarea id="ulc-rule-match" placeholder="www.example.com\n*example.com\nhttps://www.youtube.com/watch*">${matchPatterns}</textarea>
                <div class="ulc-form-hint">
                  <b class="ulc-hint-title">常用示例:</b>
                  <div class="ulc-hint-line"><code>www.example.com</code><span>仅匹配指定子域名 (推荐)</span></div>
                  <div class="ulc-hint-line"><code>*example.com</code><span>匹配主域名及其所有子域名</span></div>
                  <b class="ulc-hint-title">进阶示例:</b>
                  <div class="ulc-hint-line"><code>https://www.youtube.com/watch*</code><span>匹配特定开头的路径</span></div>
                  <div class="ulc-hint-line"><code>re:[^/]+\\.example\\.com/path/</code><span>使用正则表达式</span></div>
                </div>
                <label for="ulc-transform-keys">跳转参数 (可选, 每行一个)</label>
                <textarea id="ulc-transform-keys" placeholder="例如: target\nurl\nto">${transformKeys}</textarea>
                <p>部分网站跳转外链的时候会跳转到一个确认网页,配置参数会把对应参数内的外链直接转换为可点击链接。</p>
              </div>
              <div class="ulc-form-actions">
                <button id="ulc-cancel-add-rule-btn" class="ulc-btn-secondary">取消</button>
                <button id="ulc-save-rule-btn" class="ulc-btn-primary">保存规则</button>
              </div>
            </div>`;
          },

          renderConfigTextForm() {
            const configString = JSON.stringify(State.config, null, 2);
            return `
            <div class="ulc-main-content">
              <div class="ulc-header"><h3>配置文本</h3></div>
              <div class="ulc-form-content">
                <textarea id="ulc-config-textarea">${configString}</textarea>
              </div>
              <div class="ulc-form-actions">
                <button id="ulc-cancel-config-text-btn" class="ulc-btn-secondary">取消</button>
                <button id="ulc-save-config-text-btn" class="ulc-btn-primary">保存</button>
              </div>
            </div>`;
          }
        };

        // --- 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();
})();