Greasy Fork

Greasy Fork is available in English.

URLCleaner - 通用链接净化

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

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

您需要先安装一个扩展,例如 篡改猴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.2
// @description  自动净化链接,移除烦人的追踪参数,让您的网络足迹更干净、隐私更安全。性能至上,静默运行,对网页零侵入。支持灵活的自定义规则,是您掌控链接、保护隐私的终极利器。
// @author       You Boy
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addValueChangeListener
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const IS_DEBUG = false;   // 是否开启调试模式

  if (window.self !== window.top) {
    if (IS_DEBUG) {
      console.log(
        '%cURLCleaner%c[Sandbox]%c Skipped in iframe: %s',
        'background:#00a1d6;color:white;border-radius:3px;padding:2px 6px;',
        'background:#7f8c8d;color:white;border-radius:3px;padding:1px 4px;font-size:0.8em;margin-left:4px;',
        'color:grey;',
        window.location.href
      );
    }
    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'));
      });

      // 监听存储变化
      GM_addValueChangeListener('ulcConfig', (name, old_value, new_value, remote) => {
        if (remote) {
          this.config = new_value;

          // 通知更新
          window.dispatchEvent(new CustomEvent('ulc-config-updated', {
            detail: new_value
          }));
        }
      });
    }
  };

  const StyleInjector = {
    inject(PANEL_ID) {
      const containerID = `#${PANEL_ID}`;
      GM_addStyle(`
        ${containerID} { 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; }
        ${containerID} *, ${containerID} *::before, ${containerID} *::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; }
        ${containerID} div, ${containerID} span, ${containerID} ul, ${containerID} li, ${containerID} label { all: unset; box-sizing: border-box; }
        ${containerID} h3 { all: unset; box-sizing: border-box; display: block; font-size: 16px; font-weight: 600; }
        ${containerID} 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; }
        ${containerID} input, ${containerID} 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; }
        ${containerID} 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; }
        ${containerID} textarea { min-height: 80px; resize: vertical; }
        ${containerID} textarea::placeholder { white-space: pre-wrap; word-wrap: break-word; }
        ${containerID} code { width: initial; height: initial; display: initial; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; }
        ${containerID} button.ulc-btn-primary { background-color: #00a1d6; color: #fff; padding-block: 12px; }
        ${containerID} button.ulc-btn-primary:hover { background-color: #00b5e5; }
        ${containerID} button.ulc-btn-secondary { background-color: #fff; color: #767676; border: 1px solid #e3e3e3; }
        ${containerID} button.ulc-btn-secondary:hover { background-color: #e0e0e0; }
        ${containerID} button.ulc-btn-danger { border: 1px solid #ff4d4d; color: #ff4d4d; }
        ${containerID} button.ulc-btn-danger:hover { background-color: #ff4d4d; color: white; }
        ${containerID} .ulc-sidebar { display:flex; width: 180px; border-right: 1px solid #eee; flex-shrink: 0; flex-direction: column; }
        ${containerID} .ulc-search-container { padding: 10px 15px 0; }
        ${containerID} #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; }
        ${containerID} .ulc-tabs { display: block; list-style: none; padding: 13px 0 10px; flex-grow: 1; overflow-y: auto; }
        ${containerID} .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; }
        ${containerID} .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; }
        ${containerID} .ulc-tab:hover { background: #f5f5f5; }
        ${containerID} .ulc-tab.active { font-weight: 600; color: #00a1d6; }
        ${containerID} .ulc-tab.active::before { background-color: #00a1d6; }
        ${containerID} .ulc-tab > span { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        ${containerID} #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; }
        ${containerID} #ulc-add-rule-btn::after { content: ' 新增规则'; }
        ${containerID} #ulc-add-rule-btn:hover { background: #f0f0f0; }
        ${containerID} .ulc-main-content { display:flex; flex-grow: 1; flex-direction: column; overflow: hidden; }
        ${containerID} .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; }
        ${containerID} .ulc-title-container { display: flex; align-items: center; flex-grow: 1; max-width: 80%; }
        ${containerID} .ulc-title-container > h3 { max-width: 80%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        ${containerID} .ulc-edit-icon { display: none; cursor: pointer; margin-left: 8px; width: 16px; height: 16px; vertical-align: middle; }
        ${containerID} .ulc-title-container:hover .ulc-edit-icon { display: inline-block; }
        ${containerID} #ulc-close-btn { font-size: 24px; cursor: pointer; color: #999; padding: 5px; line-height: 1; flex-shrink: 0; }
        ${containerID} #ulc-close-btn:hover { color: #333; }
        ${containerID} .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; }
        ${containerID} .ulc-sub-header > span { display: inline; flex-shrink: 0; margin-right: 8px; line-height: 22px; }
        ${containerID} .ulc-match-tags { display: flex; flex-wrap: wrap; gap: 6px; max-height: 26px; overflow: hidden; transition: max-height 0.3s ease; flex-grow: 1; }
        ${containerID} .ulc-match-tags:hover { max-height: 200px; }
        ${containerID} .ulc-match-tags code { display: inline; background: #e9e9e9; color: #c7254e; padding: 2px 6px; border-radius: 4px; font-size: 12px; white-space: nowrap; }
        ${containerID} .ulc-add { display: flex; align-items: center; padding: 10px 15px; border-bottom: 1px solid #eee; flex-shrink: 0; }
        ${containerID} #ulc-new-param { margin-right: 10px; padding: 8px; margin-bottom: 0; }
        ${containerID} .ulc-list { display: flex; padding: 10px; overflow-y: auto; flex-grow: 1; flex-wrap: wrap; align-content: flex-start; }
        ${containerID} .ulc-list:empty::before { content: "未添加参数"; display: block; width: 100%; text-align: center; color: #999; font-size: 14px; padding: 20px; }
        ${containerID} .ulc-list-transform { position: relative; max-height: 100px; }
        ${containerID} .ulc-list-transform::before { content:"跳转参数"; display: inline-block; background: #FFF; position: absolute; top: -10px; left: 10px; padding: 0 5px; font-size: 12px; color: #999; }
        ${containerID} .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%; }
        ${containerID} .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; }
        ${containerID} .ulc-param { display: inline-flex; align-items: center; background: #eef0f2; color: #333; padding: 5px 10px; border-radius: 6px; margin: 5px; font-size: 14px; }
        ${containerID} .ulc-param span { display: inline; margin-right: 8px; }
        ${containerID} .ulc-delete { color: #999; cursor: pointer; font-weight: bold; font-size: 16px; line-height: 1; padding: 4px 8px; margin: -4px -8px; border-radius: 6px; }
        ${containerID} .ulc-delete:hover { color: #ff4d4d; }
        ${containerID} .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; }
        ${containerID} .ulc-rule-settings-footer label { display: flex; align-items: center; cursor: pointer; }
        ${containerID} .ulc-rule-settings-footer input[type="checkbox"]:checked { background-color: #00a1d6; border-color: #00a1d6; }
        ${containerID} .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; }
        ${containerID} .ulc-rule-settings-footer #ulc-config-text-btn { border-style: dashed; }
        ${containerID} .ulc-form-content { display: block; padding: 8px; flex-grow: 1; overflow-y: auto; }
        ${containerID} .ulc-form-content label { display: block; margin-bottom: 8px; font-weight: 500; margin-top: 3em; }
        ${containerID} .ulc-form-content label:first-child { margin-top: 0; }
        ${containerID} .ulc-form-content p { font-size: 12px; display: block; color: #999; }
        ${containerID} .ulc-form-actions { display: flex; padding: 15px; border-top: 1px solid #eee; justify-content: flex-end; gap: 10px; flex-shrink: 0; }
        ${containerID} .ulc-form-hint { display: block; font-size: 12px; color: #666; margin-top: -5px; margin-bottom: 15px; }
        ${containerID} .ulc-hint-title { display: block; font-weight: bold; margin-top: 8px; }
        ${containerID} .ulc-hint-line { display: flex; align-items: center; margin-top: 4px; }
        ${containerID} .ulc-hint-line code { display: inline-block; flex-shrink: 0; background: #f5f5f5; color: #c7254e; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
        ${containerID} .ulc-hint-line span { display: inline; margin-left: 8px; }
        ${containerID} #ulc-config-textarea { height: 100%; min-height: 100px; resize: vertical; margin-bottom: 0; }
        ${containerID} #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; }
        ${containerID} #ulc-toast.show { opacity: 1; visibility: visible; }
        ${containerID} .ulc-confirming-action { background-color: #ff4d4d !important; border-color: #ff4d4d !important; color: white !important; }
        ${containerID} .ulc-confirming-action:hover { background-color: #e60000 !important; }
        ${containerID} .ulc-confirming-action, ${containerID} .ulc-confirmation-activating { position: relative; }
        ${containerID} .ulc-confirming-action:hover::before, ${containerID} .ulc-confirming-action:focus::before { content: attr(data-confirm-desc); position: absolute; bottom: 100%; right: 0;  margin-bottom: 8px; background: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 13px; z-index: 1; pointer-events: none; max-width: 300px; min-width: 220px; white-space: normal; text-align: left; line-height: 1.4; }
        ${containerID} .ulc-confirming-action:hover::after, ${containerID} .ulc-confirming-action:focus::after { content: ''; position: absolute; bottom: 100%; right: 50%; transform: translateX(-50%); margin-bottom: -4px; border: 6px solid transparent; border-top-color: #333; z-index: 1; pointer-events: none; }
        ${containerID} .ulc-confirmation-activating { cursor: wait; animation: ulc-pulse 0.5s ease-out; }
        @keyframes ulc-pulse {
          0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.7); }
          50% { transform: scale(1.02); box-shadow: 0 0 0 8px rgba(255, 77, 77, 0); }
          100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 77, 77, 0); }
        }
        
        @media (max-width: 600px) {
          ${containerID} { width: 100vw; height: 100vh; max-height: 100vh; min-width: 0; border-radius: 0; flex-direction: column; }
          ${containerID} .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; }
          ${containerID} .ulc-search-container { order: 1; flex-grow: 1; padding: 0; }
          ${containerID} #ulc-rule-search { font-size: 15px; padding: 10px 12px; }
          ${containerID} #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; }
          ${containerID} #ulc-add-rule-btn::after { font-size: 20px; content: '+'; }
          ${containerID} .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; }
          ${containerID} .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; }
          ${containerID} .ulc-tab::before { display: none; }
          ${containerID} .ulc-tab.active { border-bottom-color: #00a1d6; color: #00a1d6; background-color: transparent; }
          ${containerID} .ulc-param { padding: 8px 12px; font-size: 15px; }
          ${containerID} .ulc-delete { padding: 8px; }
          ${containerID} .ulc-edit-icon { display:inline-block; }
          ${containerID} #ulc-toast { left: 50%; }
        }
      `);
    }
  };

  const CodeInjector = {
    injectedCode: function (sandboxConfig, sandboxDefaultConfig, { IS_DEBUG, PANEL_ID, isFallbackMode = false, sandboxUnsafeWindow }) {
      (() => {
        // 发送“心跳”信号
        window.dispatchEvent(new CustomEvent('ulc-injection-success'));

        const GENERAL_TAB_ID = 'general';

        const Logger = {
          _styles: {
            brand: 'background: #00a1d6; color: white; border-radius: 3px; padding: 2px 6px;',
            tagBase: 'color: white; border-radius: 3px; padding: 1px 5px; font-size: 0.8em; margin-left: 4px;',
            get INFO() { return `background: #3498db; ${this.tagBase}`; },
            get WARN() { return `background: #f39c12; ${this.tagBase}`; },
            get ERROR() { return `background: #e74c3c; ${this.tagBase}`; },
            get GROUP() { return `background: #95a5a6; ${this.tagBase}`; }, // 新增GROUP样式
            title: 'font-weight: bold;',
          },

          _createLog(type, isGrouped, ...args) {
            if (!IS_DEBUG) return;

            const brand = '%cURLCleaner';
            const tag = `%c${type.toUpperCase()}`;
            const brandStyle = this._styles.brand;
            const tagStyle = this._styles[type];

            if (!isGrouped) {
              console.log(brand + tag, brandStyle, tagStyle, ...args);
            } else {
              const [title, ...content] = args;
              const titleStyle = `${this._styles.title} color: ${tagStyle.match(/background: (#\w+);/)[1] || 'inherit'};`;

              console.groupCollapsed(brand + tag + `%c ${title}`, brandStyle, tagStyle, titleStyle);
              if (content.length > 0) {
                const consoleMethod = type === 'INFO' ? 'log' : type.toLowerCase();
                content.forEach(item => console[consoleMethod](item));
              }
              console.groupEnd();
            }
          },

          log(...args) { this._createLog('INFO', false, ...args); },
          warn(title, ...content) { this._createLog('WARN', true, title, ...content); },
          error(title, ...content) { this._createLog('ERROR', true, title, ...content); },

          group(title) {
            if (IS_DEBUG) {
              console.groupCollapsed(
                `%cURLCleaner%cGROUP%c ${title}`,
                this._styles.brand,
                this._styles.GROUP,
                this._styles.title
              );
            }
          },
          groupEnd() { if (IS_DEBUG) { console.groupEnd(); } },
          info(...args) { if (IS_DEBUG) { console.log(...args); } }
        };

        if (isFallbackMode) {
          Logger.log('Fallback mode activated due to CSP.');
        } else {
          Logger.log('Script injected and running in standard mode.');
        }

        // --- Utils (工具函数) ---
        const Utils = {
          debounce(func, delay = 250) {
            let timeoutId;
            return function (...args) {
              clearTimeout(timeoutId);
              timeoutId = setTimeout(() => {
                func.apply(this, args);
              }, delay);
            };
          },

          // 字符串转为正则表达式对象
          wildcardToRegex(pattern) {
            try {
              if (pattern.startsWith('re:')) {
                return new RegExp(pattern.substring(3));
              }
              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) {
              Logger.warn(`Invalid regex pattern provided, falling back to non-matching pattern.`, { pattern, error: e.message });
              return new RegExp('$.');
            }
          },

          // 严格检查是否是有效的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 {
              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 (Utils.isValidAbsoluteURL(input)) return input;
              for (const decoder of decoders) {
                try {
                  const decoded = decoder(input);
                  if (decoded && Utils.isValidAbsoluteURL(decoded)) {
                    return decoded;
                  }
                } catch (error) { /* Silently ignore decoding errors */ }
              }
              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] = url.searchParams.entries().next().value || [];
              if (url.searchParams.size === 1 && key && !url.searchParams.get(key)) {
                const decoded = Utils.tryAllDecodes(key);
                if (decoded) return decoded;
              }
            } catch (_) { /* Silently ignore parsing errors */ }
            return null;
          },

          // 生成规则的唯一ID
          getRuleTabId(ruleOrName) {
            if (typeof ruleOrName === 'string' && ruleOrName === GENERAL_TAB_ID) {
              return GENERAL_TAB_ID;
            }
            const name = (typeof ruleOrName === 'object' && ruleOrName.name) ? ruleOrName.name : ruleOrName;
            if (name === GENERAL_TAB_ID || typeof name !== 'string' || name.trim() === '') {
              return GENERAL_TAB_ID;
            }
            try {
              return `rule-${btoa(encodeURIComponent(name))}`;
            } catch (e) {
              Logger.error(`Failed to generate ID for rule name: ${name}`, e);
              return `rule-error-${Date.now()}`;
            }
          },

          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; }
          },

          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;
          },

          // 验证配置文件合法性
          validateConfigObject(config) {
            if (typeof config !== 'object' || config === null) return "配置必须是一个对象。";
            if (typeof config.general !== 'object' || config.general === null) return `配置缺少 '${GENERAL_TAB_ID}' 对象。`;
            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;
          },
        };

        // --- State (状态管理) ---
        const State = {
          config: null,
          DEFAULT_CONFIG: null,
          paramsToRemove: new Set(),
          transformKeysToUse: new Set(),
          cleanedAttrName: '',
          invalidAttrName: '',
          ui: {
            activeTab: GENERAL_TAB_ID,
            activeRuleIndex: -1,
            view: 'list', // 'list', 'add', 'edit', 'config-text'
            searchQuery: '',
            ACTIVATION_DELAY: 800,  // confirmation activation delay
          },
          dom: {
            settingsPanel: null,
            sidebarContainer: null,
            mainContentContainer: null,
            panelId: PANEL_ID,
          },
          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();
          },

          setActiveParameters() {
            const matchingRules = [];
            for (const rule of State.config.rules) {
              for (const match of rule.match) {
                if (Utils.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;

            Logger.group('Active Parameters Updated', true);
            Logger.info('URL:', window.location.href);
            Logger.info('Matching rules found:', matchingRules.map(r => r.name));
            Logger.info('Params to remove:', [...State.paramsToRemove]);
            Logger.info('Transform keys to use:', [...State.transformKeysToUse]);
            Logger.groupEnd();
          },

          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 = Utils.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 = Utils.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;
            }
          },

        };

        // --- UI (界面渲染) ---
        const UI = {
          setSafelyInnerHTML(element, htmlString) {
            if (window.trustedTypes && window.trustedTypes.createPolicy) {
              try {
                const policy = window.trustedTypes.defaultPolicy || window.trustedTypes.createPolicy('URLCleanerPolicy#html', {
                  createHTML: s => s
                });
                element.innerHTML = policy.createHTML(htmlString);
              } catch (e) {
                try {
                  element.innerHTML = htmlString;
                } catch (finalError) {
                  Logger.error('UI Rendering Blocked by CSP', 'Could not set innerHTML due to very strict Trusted Types policy.', `Initial Error: ${e.message}`, `Final Error: ${finalError.message}`);
                }
              }
            } 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 = State.dom.panelId;
            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_TAB_ID;
            generalTab.dataset.ruleIndex = '-1';
            generalTab.textContent = '通用规则';
            if (State.ui.activeTab === GENERAL_TAB_ID) {
              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 = Utils.getRuleTabId(rule);
              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 === Utils.getRuleTabId(rule)) {
                li.classList.add('active');
              }
              fragment.appendChild(li);
            });

            this.setSafelyInnerHTML(tabsContainer, '')
            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_TAB_ID;
            const rule = !isGeneral ? State.config.rules[State.ui.activeRuleIndex] : null;

            if (!isGeneral && !rule) {
              State.ui.activeTab = GENERAL_TAB_ID;
              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" data-confirm-desc="确定要将通用参数重置为默认值吗?此操作不可撤销。">重置为默认</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" data-confirm-desc="确定要删除规则 “${rule.name}” 吗?此操作不可撤销。">删除此规则</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 = {
          _getCurrentContext() {
            const isGeneral = State.ui.activeTab === GENERAL_TAB_ID;
            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
            };
          },

          _confirmingAction: {
            el: null,
            onConfirm: null,
            originalText: '',
            isActivating: false,
            timer: null,
          },

          _resetConfirmation() {
            if (this._confirmingAction.el) {
              clearTimeout(this._confirmingAction.timer);
              this._confirmingAction.el.classList.remove('ulc-confirming-action', 'ulc-confirmation-activating');
              this._confirmingAction.el.textContent = this._confirmingAction.originalText;
              this._confirmingAction = { el: null, onConfirm: null, originalText: '' };
            }
          },

          requestConfirmation(buttonElement, onConfirmCallback, confirmText = '确认?') {
            this._resetConfirmation();

            this._confirmingAction = {
              el: buttonElement,
              onConfirm: onConfirmCallback,
              originalText: buttonElement.textContent,
              isActivating: true,
              timer: setTimeout(() => {
                this._confirmingAction.isActivating = false;
                if (this._confirmingAction.el) {
                  this._confirmingAction.el.classList.remove('ulc-confirmation-activating');
                }
              }, State.ui.ACTIVATION_DELAY)
            };
            buttonElement.classList.add('ulc-confirming-action', 'ulc-confirmation-activating');
            buttonElement.textContent = confirmText;
          },

          addParamsFromInput() {
            const input = document.getElementById('ulc-new-param');
            if (!input || !input.value) return false;

            const inputValue = input.value.trim();
            let newParams = [];

            const parsedUrl = Utils.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;
            }

            Logger.log(`Added ${addedCount} parameter(s) to "${context.isGeneral ? 'General Rules' : context.rule.name}".`, newParams.filter(p => !new Set(paramsList).has(p)));
            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);

              const contextName = context.isGeneral ? 'General Rules' : context.rule.name;
              Logger.log(`Parameter "${paramToDelete}" deleted from "${contextName}".`);
              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)) {
              Logger.warn('Save failed: Duplicate rule name detected.', newName);
              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 = Utils.getRuleTabId(newRule);
            }
            Logger.log(`Rule "${newName}" has been saved (${isEdit ? 'edited' : 'newly created'}).`);
            UI.showToast(`规则 "${newName}" 已保存`);
            return true;
          },

          deleteCurrentRule() {
            const rule = State.config.rules[State.ui.activeRuleIndex];
            if (State.ui.activeTab === GENERAL_TAB_ID || !rule) return false;
            State.config.rules.splice(State.ui.activeRuleIndex, 1);
            Logger.log(`Rule "${rule.name}" has been deleted.`);
            UI.showToast('已删除');
            return true;
          },

          saveConfigFromText() {
            const textarea = document.getElementById('ulc-config-textarea');
            if (!textarea) return false;
            let newConfig;

            try {
              newConfig = JSON.parse(textarea.value);
            } catch (e) {
              Logger.error('Failed to parse configuration text as JSON.', { error: e.message, text: textarea.value });
              UI.showToast('JSON 格式无效,请检查您的输入。\n错误信息: ' + e.message, 3000);
              return false;
            }

            const validationError = Utils.validateConfigObject(newConfig);
            if (validationError) {
              Logger.warn('Save from text failed: Invalid configuration.', { error: validationError, config: newConfig });
              UI.showToast('配置结构不正确:\n' + validationError, 4000);
              return false;
            }

            State.config.general = newConfig.general;
            State.config.rules = newConfig.rules;

            UI.showToast('配置已成功保存');
            return true;
          },

          resetConfig() {
            State.config.general.params = JSON.parse(JSON.stringify(State.DEFAULT_CONFIG.general.params));
            Logger.warn('General rules have been reset to default.');
            UI.showToast('通用参数已重置为默认');
            return true;
          },

          toggleApplyGeneral(isChecked) {
            if (State.ui.activeTab !== GENERAL_TAB_ID && 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 (this._confirmingAction.el && this._confirmingAction.el === target) {
              if (this._confirmingAction.isActivating) {
                return;
              }
              this._confirmingAction.onConfirm();
              this._resetConfirmation();
              return;
            }
            if (this._confirmingAction.el) {
              this._resetConfirmation();
            }

            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')) {
              const deleteBtn = closest('#ulc-delete-rule-btn');
              this.requestConfirmation(deleteBtn, () => {
                if (this.deleteCurrentRule()) {
                  State.ui.activeTab = GENERAL_TAB_ID;
                  State.ui.activeRuleIndex = -1;
                  Core.saveConfig();
                  UI.renderPanel();
                }
              }, '确认删除?');
            } else if (closest('#ulc-reset-btn')) {
              const resetBtn = closest('#ulc-reset-btn');
              this.requestConfirmation(resetBtn, () => {
                if (this.resetConfig()) {
                  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_TAB_ID; 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(`#${State.dom.panelId}`)) return;
              const link = e.target.closest('a[href]');
              if (link && !link.dataset[State.cleanedAttrName] && !link.dataset[State.invalidAttrName]) {
                if (Utils.isValidHttpLink(link)) {
                  const cleanedHref = Core.cleanUrl(link.href);
                  if (link.href !== cleanedHref) {
                    Logger.log('Link purified on hover:', { from: link.href, to: 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) {
                  Logger.warn('Link purified on click (final fix):', { from: link.href, to: 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 originalUrl = url ? url.toString() : '';
                const cleanedUrl = Core.cleanUrl(originalUrl);

                if (originalUrl !== cleanedUrl) {
                  Logger.log(`history.${method} intercepted and URL purified.`, {
                    from: originalUrl,
                    to: cleanedUrl,
                    state: state
                  });
                }

                const oldHref = window.location.href;
                const result = original.apply(this, [state, title, cleanedUrl, ...rest]);

                requestAnimationFrame(() => {
                  // 重新计算规则
                  if (window.location.href !== oldHref) {
                    Logger.log('SPA navigation detected. Recalculating parameters...');
                    Core.setActiveParameters();
                  }
                });

                return result;
              };
            };

            wrapHistoryMethod('pushState');
            wrapHistoryMethod('replaceState');

            // 沙盒模式下window.open无法正确拦截,所以需要使用unsafeWindow
            const openContext = isFallbackMode ? sandboxUnsafeWindow : window;
            if (openContext) {
              const originalOpen = openContext.open;
              if (typeof originalOpen === 'function') {
                const originalOpen = openContext.open;
                openContext.open = function (url, ...args) {
                  const originalUrl = url ? url.toString() : '';
                  const cleanedUrl = Core.cleanUrl(originalUrl);

                  if (originalUrl !== cleanedUrl) {
                    Logger.log('window.open call intercepted and URL purified.', {
                      from: originalUrl,
                      to: cleanedUrl,
                      target: args[0] || '_blank'
                    });
                  }
                  return originalOpen.apply(openContext, [cleanedUrl, ...args]);
                };
              } else {
                Logger.warn('window.open is not a function and could not be patched.');
              }
            } else {
              Logger.warn('window context is not available for patching window.open.');
            }

            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_TAB_ID;
                State.ui.activeRuleIndex = -1;
                UI.renderPanel();
                State.dom.settingsPanel.style.display = 'flex';
              };
              document.body ? open() : document.addEventListener('DOMContentLoaded', open);
            });

            window.addEventListener('ulc-config-updated', (event) => {
              Logger.log('Configuration synced from another tab. Updating state...');
              const newConfig = event.detail;

              // 更新配置
              State.config = newConfig;
              Core.setActiveParameters();

              if (State.dom.settingsPanel) {
                State.ui.view = 'list';
                if (State.ui.activeTab !== GENERAL_TAB_ID) {
                  const ruleIndex = State.config.rules.findIndex(r => Utils.getRuleTabId(r) === State.ui.activeTab);
                  if (ruleIndex === -1) {
                    State.ui.activeTab = GENERAL_TAB_ID;
                    State.ui.activeRuleIndex = -1;
                  } else {
                    State.ui.activeRuleIndex = ruleIndex;
                  }
                }
                UI.renderPanel();
              }
            });
          }
        };

        // --- 初始化 ---
        function main() {
          const config = sandboxConfig;
          const defaultConfig = sandboxDefaultConfig;
          State.init(config, defaultConfig);

          // --- 初始化防抖的搜索处理器 ---
          Events.onSearchInputDebounced = Utils.debounce((e) => {
            Events._performSearch(e.target.value);
          }, 250);

          State.cleanedAttrName = Utils.randomString();
          State.invalidAttrName = Utils.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, injectedConfig = {}) {
      const nonce = document.querySelector('script[nonce]')?.nonce || document.querySelector('style[nonce]')?.nonce;
      const finalCodeString = `(${this.injectedCode.toString()})(${JSON.stringify(config)}, ${JSON.stringify(defaultConfig)}, ${JSON.stringify(injectedConfig)});`;
      const injectedScript = document.createElement('script');
      injectedScript.id = 'ulc-injected-script';
      injectedScript.nonce = nonce;

      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() {
    const PANEL_ID = 'ulc-panel-' + Math.random().toString(36).substring(2, 10);

    Sandbox.loadConfig();
    Sandbox.init();
    StyleInjector.inject(PANEL_ID);
    let injectionSuccessful = false;

    const successListener = () => {
      injectionSuccessful = true;
      window.removeEventListener('ulc-injection-success', successListener);
    };
    window.addEventListener('ulc-injection-success', successListener);

    // 注入配置项
    const INJECT_CONFIG = {
      IS_DEBUG,
      PANEL_ID,
    }

    // 注入模式
    CodeInjector.inject(Sandbox.config, Sandbox.DEFAULT_CONFIG, INJECT_CONFIG);

    setTimeout(() => {
      if (!injectionSuccessful) {
        // 注入失败,降级为沙盒模式
        window.removeEventListener('ulc-injection-success', successListener);
        CodeInjector.injectedCode(Sandbox.config, Sandbox.DEFAULT_CONFIG, {
          ...INJECT_CONFIG,
          isFallbackMode: true,
          sandboxUnsafeWindow: unsafeWindow
        });
      }
    }, 0);
  }

  main();
})();