Greasy Fork

Greasy Fork is available in English.

URLCleaner - 通用链接净化

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name              URLCleaner - Universal Link Purifier
// @name:zh-CN        URLCleaner - 通用链接净化
// @name:zh-TW        URLCleaner - 通用鏈接淨化
// @name:de           URLCleaner - Universeller Link-Reiniger
// @name:es           URLCleaner - Purificador de Enlaces Universal
// @name:fr           URLCleaner - Purificateur de Liens Universel
// @name:ja           URLCleaner - 汎用リンククリーナー
// @name:ko           URLCleaner - 범용 링크 정화기
// @name:ru           URLCleaner - Универсальный очиститель ссылок
// @name:pt           URLCleaner - Purificador de Links Universal
// @namespace         You Boy
// @version           1.4
// @description       Automatically purges annoying tracking parameters from links, making your web footprint cleaner and your privacy more secure. Performance-first, silent operation, and zero intrusion on web pages. With flexible custom rules, it's your ultimate tool for controlling links and protecting privacy.
// @description:zh-CN 自动净化链接,移除烦人的追踪参数,让您的网络足迹更干净、隐私更安全。性能至上,静默运行,对网页零侵入。支持灵活的自定义规则,是您掌控链接、保护隐私的终极利器。
// @description:zh-TW 自動淨化鏈接,移除煩人的追踪參數,讓您的網絡足跡更乾淨、隱私更安全。性能至上,靜默運行,對網頁零侵入。支持靈活的自定義規則,是您掌控鏈接、保護隱私的終極利器。
// @description:de    Entfernt automatisch lästige Tracking-Parameter aus Links, macht Ihren Web-Fußabdruck sauberer und schützt Ihre Privatsphäre. Leistungsorientiert, leiser Betrieb und kein Eingriff in Webseiten. Mit flexiblen benutzerdefinierten Regeln ist es Ihr ultimatives Werkzeug zur Steuerung von Links und zum Schutz der Privatsphäre.
// @description:es    Purga automáticamente los molestos parámetros de seguimiento de los enlaces, haciendo su huella web más limpia y su privacidad más segura. Prioriza el rendimiento, operación silenciosa y cero intrusión en las páginas web. Con reglas personalizadas flexibles, es su herramienta definitiva para controlar enlaces y proteger la privacidad.
// @description:fr    Purge automatiquement les paramètres de suivi gênants des liens, rendant votre empreinte web plus propre et votre vie privée plus sûre. Axé sur la performance, fonctionnement silencieux et aucune intrusion sur les pages web. Avec des règles personnalisées flexibles, c'est votre outil ultime pour contrôler les liens et protéger votre vie privée.
// @description:ja    リンクから迷惑な追跡パラメータを自動的に除去し、ウェブ上の足跡をよりクリーンに、プライバシーをより安全にします。パフォーマンス第一、静かな動作、ウェブページへのゼロ侵入。柔軟なカスタムルールで、リンクをコントロールし、プライバシーを保護するための究極のツールです。
// @description:ko    링크에서 성가신 추적 매개변수를 자동으로 제거하여 웹 사용 기록을 더 깨끗하게 만들고 개인 정보를 더 안전하게 보호합니다. 성능 우선, 조용한 작동 및 웹 페이지에 대한 간섭 없음. 유연한 사용자 지정 규칙을 통해 링크를 제어하고 개인 정보를 보호하는 최고의 도구입니다.
// @description:ru    Автоматически удаляет назойливые параметры отслеживания из ссылок, делая ваш след в сети чище и обеспечивая вашу конфиденциальность. Производительность прежде всего, бесшумная работа и нулевое вторжение на веб-страницы. С гибкими настраиваемыми правилами это ваш незаменимый инструмент для контроля ссылок и защиты конфиденциальности.
// @description:pt    Purifica automaticamente os irritantes parâmetros de rastreamento dos links, tornando sua pegada na web mais limpa e sua privacidade mais segura. Foco no desempenho, operação silenciosa e zero intrusão nas páginas da web. Com regras personalizadas flexíveis, é a sua ferramenta definitiva para controlar links e proteger a privacidade.
// @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 = true;

  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;
  }
  // --- I18n Manager ---
  const I18nManager = {
    DEFAULT_LANG: 'en',

    // 所有语言文本包
    locales: {
      'zh-CN': {
        menu: {
          settings: '设置',
        },
        ui: {
          titles: {
            generalList: '通用参数列表',
            addRule: '新增净化规则',
            editRule: '编辑净化规则',
            configText: '配置文本',
          },
          tabs: {
            general: '通用规则',
          },
          buttons: {
            add: '添加',
            save: '保存',
            saveRule: '保存规则',
            cancel: '取消',
            delete: '删除此规则',
            reset: '重置为默认',
            addRule: '新增规则',
            configText: '配置文本',
            confirm: '确认?',
            confirmReset: '确认重置?',
            confirmDelete: '确认删除?',
          },
          labels: {
            ruleName: '规则名称',
            matchAddress: '匹配地址 (每行一个)',
            matchAddressShort: '匹配地址',
            transformKeys: '跳转参数 (可选, 每行一个)',
            applyGeneral: '应用通用规则',
            enableRule: '启用规则',
          },
          placeholders: {
            ruleName: '规则名称',
            matchAddress: 'www.example.com\n*example.com\nhttps://www.youtube.com/watch*',
            transformKeys: '例如: target\nurl\nto',
            addParam: '输入参数,可英文逗号分隔批量添加,或输入一个链接自动提取',
            search: '搜索规则',
          },
          hints: {
            matchAddress: `<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>`,
            transform: '部分网站跳转外链的时候会跳转到一个确认网页,配置参数会把对应参数内的外链直接转换为可点击链接。'
          },
          misc: {
            noParams: '未添加参数',
            transformTitle: '跳转参数'
          }
        },
        toasts: {
          extracted: '已从链接中提取 {count} 个参数',
          allEmpty: '规则名称和匹配地址不能为空。',
          nameReserved: '错误:“general”是保留名称,请使用其他名称。',
          nameExists: '错误:已存在同名规则,请使用其他名称。',
          ruleSaved: '规则 “{ruleName}” 已保存',
          ruleDeleted: '已删除',
          configSaved: '配置已成功保存',
          configReset: '通用参数已重置为默认',
          jsonInvalid: 'JSON 格式无效,请检查您的输入。\n错误信息: {error}',
          configInvalid: '配置结构不正确:\n{error}',
        },
        prompts: {
          deleteRule: '确定要删除规则 “{ruleName}” 吗?此操作不可撤销。',
          resetGeneral: '确定要将通用参数重置为默认值吗?此操作不可撤销。',
        }
      },
      'en': {
        menu: {
          settings: 'Settings',
        },
        ui: {
          titles: {
            generalList: 'General Parameters List',
            addRule: 'Add New Rule',
            editRule: 'Edit Rule',
            configText: 'Configuration Text',
          },
          tabs: {
            general: 'General Rules',
          },
          buttons: {
            add: 'Add',
            save: 'Save',
            saveRule: 'Save Rule',
            cancel: 'Cancel',
            delete: 'Delete This Rule',
            reset: 'Reset to Default',
            addRule: 'New Rule',
            configText: 'Config Text',
            confirm: 'Confirm?',
            confirmReset: 'Confirm Reset?',
            confirmDelete: 'Confirm Delete?',
          },
          labels: {
            ruleName: 'Rule Name',
            matchAddress: 'Match URLs (one per line)',
            matchAddressShort: 'Match URLs',
            transformKeys: 'Redirect Keys (optional, one per line)',
            applyGeneral: 'Apply general rules',
            enableRule: 'Enable Rule',
          },
          placeholders: {
            ruleName: 'Rule Name',
            matchAddress: 'www.example.com\n*example.com\nhttps://www.youtube.com/watch*',
            transformKeys: 'e.g., target\nurl\nto',
            addParam: 'Enter parameter(s), or paste a URL to extract from',
            search: 'Search rules',
          },
          hints: {
            matchAddress: `<b class="ulc-hint-title">Common Examples:</b><div class="ulc-hint-line"><code>www.example.com</code><span>Matches specific subdomain (recommended)</span></div><div class="ulc-hint-line"><code>*example.com</code><span>Matches main domain and all subdomains</span></div><b class="ulc-hint-title">Advanced Examples:</b><div class="ulc-hint-line"><code>https://www.youtube.com/watch*</code><span>Matches a specific path prefix</span></div><div class="ulc-hint-line"><code>re:[^/]+\\.example\\.com/path/</code><span>Use a regular expression</span></div>`,
            transform: 'For redirect pages that encode the destination URL in a parameter. This will convert the link directly to the destination.'
          },
          misc: {
            noParams: 'No parameters added',
            transformTitle: 'Redirect Keys'
          }
        },
        toasts: {
          extracted: 'Extracted {count} parameter(s) from the link',
          allEmpty: 'Rule Name and Match URLs cannot be empty.',
          nameReserved: 'Error: "general" is a reserved name. Please use another name.',
          nameExists: 'Error: A rule with the same name already exists. Please use another name.',
          ruleSaved: 'Rule "{ruleName}" has been saved',
          ruleDeleted: 'Deleted',
          configSaved: 'Configuration saved successfully',
          configReset: 'General parameters have been reset to default',
          jsonInvalid: 'Invalid JSON format. Please check your input.\nError: {error}',
          configInvalid: 'Invalid configuration structure:\n{error}',
        },
        prompts: {
          deleteRule: 'Are you sure you want to delete the rule "{ruleName}"? This action cannot be undone.',
          resetGeneral: 'Are you sure you want to reset general parameters to default? This action cannot be undone.',
        }
      }
    },

    detectLanguage() {
      const lang = navigator.language;
      if (this.locales[lang]) {
        return lang;
      }
      const baseLang = lang.split('-')[0];
      const matchedLang = Object.keys(this.locales).find(l => l.startsWith(baseLang));
      return matchedLang || this.DEFAULT_LANG;
    },

    getActiveLocale() {
      const langKey = this.detectLanguage();
      return this.locales[langKey];
    }
  };

  // --- 沙箱环境 ---
  const Sandbox = {
    DEFAULT_CONFIG: {
      general: { params: ['spm_id_from', 'from_source', 'utm_source'] },
      rules: []
    },

    config: null,

    loadConfig() {
      let config;
      try {
        config = GM_getValue('ulcConfig');
      } catch (e) {
        config = null;
      }

      if (typeof config !== 'object' || config === null || Array.isArray(config)) {
        if (IS_DEBUG) {
          console.warn('URLCleaner: Invalid config format in GM_storage. Resetting to a structurally safe default.', { received: config });
        }
        config = structuredClone(this.DEFAULT_CONFIG);
      }

      if (typeof config.general !== 'object' || config.general === null || Array.isArray(config.general)) {
        config.general = { params: [] };
      }

      if (!Array.isArray(config.general.params)) {
        config.general.params = [];
      }

      if (!Array.isArray(config.rules)) {
        config.rules = [];
      }

      this.config = config;
    },

    init(locale) {
      // 监听来自注入代码的保存请求
      window.addEventListener('ulc-save-config', (event) => {
        GM_setValue('ulcConfig', event.detail);
      });

      // 注册油猴菜单
      GM_registerMenuCommand(locale.menu.settings, () => {
        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} {
          --ulc-bg-primary: #fff;
          --ulc-bg-secondary: #f9f9f9;
          --ulc-bg-input: #fff;
          --ulc-bg-param: #eef0f2;
          --ulc-bg-param-transform: #fceeee;
          --ulc-bg-code: #e9e9e9;
          --ulc-bg-code-hint: #f5f5f5;
          --ulc-bg-tab-hover: #f5f5f5;
          --ulc-bg-add-rule-btn: #fafafa;
          --ulc-bg-add-rule-btn-hover: #f0f0f0;
          --ulc-bg-secondary-btn: #fff;
          --ulc-bg-secondary-btn-hover: #e0e0e0;
          --ulc-bg-danger-btn-hover: #ff4d4d;
          --ulc-bg-confirm-tooltip: #333;
          --ulc-bg-mobile-add-btn: #fff;
          --ulc-text-primary: #333;
          --ulc-text-secondary: #666;
          --ulc-text-tertiary: #999;
          --ulc-text-placeholder: #888;
          --ulc-text-param: #333;
          --ulc-text-param-transform: #333;
          --ulc-text-code: #c7254e;
          --ulc-text-add-rule-btn: #333;
          --ulc-text-close-btn: #999;
          --ulc-text-close-btn-hover: #333;
          --ulc-text-delete-icon: #999;
          --ulc-text-delete-icon-hover: #ff4d4d;
          --ulc-text-secondary-btn: #767676;
          --ulc-text-danger: #ff4d4d;
          --ulc-text-danger-btn-hover: white;
          --ulc-border-primary: #eee;
          --ulc-border-secondary: #e3e3e3;
          --ulc-border-input: #ccc;
          --ulc-border-checkbox: #ccc;
          --ulc-border-danger-btn: #ff4d4d;
          --ulc-border-mobile-add-btn: #ddd;
          --ulc-accent-primary: #00a1d6;
          --ulc-accent-hover: #00b5e5;
          --ulc-accent-static: #00a1d6;
          --ulc-scrollbar-bg: #f0f2f5;
          --ulc-scrollbar-thumb-bg: #c1c1c1;
          --ulc-scrollbar-thumb-hover-bg: #a8a8a8;
        }
        ${containerID}.theme-dark {
          --ulc-bg-primary: #2c2c2c;
          --ulc-bg-secondary: #3a3a3a;
          --ulc-bg-input: #252525;
          --ulc-bg-param: #444;
          --ulc-bg-param-transform: #6c3838;
          --ulc-bg-code: #444;
          --ulc-bg-code-hint: #444;
          --ulc-bg-tab-hover: #383838;
          --ulc-bg-add-rule-btn: #333;
          --ulc-bg-add-rule-btn-hover: #404040;
          --ulc-bg-secondary-btn: #4f4f4f;
          --ulc-bg-secondary-btn-hover: #5a5a5a;
          --ulc-bg-danger-btn-hover: #e53935;
          --ulc-text-primary: #dcdcdc;
          --ulc-text-secondary: #bbb;
          --ulc-text-tertiary: #aaa;
          --ulc-text-placeholder: #888;
          --ulc-text-param: #eee;
          --ulc-text-param-transform: #e0c7c7;
          --ulc-text-code: #ff8a65;
          --ulc-text-add-rule-btn: #bbb;
          --ulc-text-close-btn: #aaa;
          --ulc-text-close-btn-hover: #fff;
          --ulc-text-delete-icon: #aaa;
          --ulc-text-delete-icon-hover: #e53935;
          --ulc-text-secondary-btn: #dcdcdc;
          --ulc-text-danger: #e53935;
          --ulc-text-danger-btn-hover: #fff;
          --ulc-border-primary: #4a4a4a;
          --ulc-border-secondary: #666;
          --ulc-border-input: #555;
          --ulc-border-checkbox: #888;
          --ulc-border-danger-btn: #e53935;
          --ulc-border-mobile-add-btn: #555;
          --ulc-accent-primary: #008fbf;
          --ulc-accent-hover: #00a1d6;
          --ulc-accent-static: #00a1d6;
          --ulc-scrollbar-bg: #2c2c2c;
          --ulc-scrollbar-thumb-bg: #555;
          --ulc-scrollbar-thumb-hover-bg: #777;
        }
        ${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: var(--ulc-bg-primary); 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: var(--ulc-text-primary); font-size: 14px; }
        ${containerID}.theme-dark { border: 1px solid var(--ulc-border-primary); }
        ${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 var(--ulc-border-input); 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: var(--ulc-bg-input); line-height: 1.4; color: var(--ulc-text-primary); }
        ${containerID} input::placeholder, ${containerID} textarea::placeholder { color: var(--ulc-text-placeholder); }
        ${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: var(--ulc-accent-primary); color: #fff; padding-block: 12px; }
        ${containerID} button.ulc-btn-secondary { background-color: var(--ulc-bg-secondary-btn); color: var(--ulc-text-secondary-btn); border: 1px solid var(--ulc-border-secondary); }
        ${containerID} button.ulc-btn-danger { border: 1px solid var(--ulc-border-danger-btn); color: var(--ulc-text-danger); }
        ${containerID} .ulc-sidebar { display:flex; width: 180px; border-right: 1px solid var(--ulc-border-primary); flex-shrink: 0; flex-direction: column; }
        ${containerID} .ulc-search-container { padding: 10px 15px 0; }
        ${containerID} .ulc-search-container input[type="search"] { all: unset; box-sizing: border-box; width: 100%; border: 1px solid var(--ulc-border-input); border-radius: 4px; padding: 6px 10px; font-size: 13px; background-color: var(--ulc-bg-input); }
        ${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.active { font-weight: 600; color: var(--ulc-accent-static); }
        ${containerID} .ulc-tab.active::before { background-color: var(--ulc-accent-static); }
        ${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: var(--ulc-bg-add-rule-btn); border-top: 1px solid var(--ulc-border-primary); color: var(--ulc-text-add-rule-btn); font-size: 14px; flex-shrink: 0; }
        ${containerID} #ulc-add-rule-btn::after { content: attr(data-locale); }
        ${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 var(--ulc-border-primary); 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: var(--ulc-text-close-btn); padding: 5px; line-height: 1; flex-shrink: 0; }
        ${containerID} .ulc-sub-header { display: flex; align-items: flex-start; justify-content: flex-start; padding: 8px 15px; background: var(--ulc-bg-secondary); border-bottom: 1px solid var(--ulc-border-primary); font-size: 12px; color: var(--ulc-text-secondary); 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: var(--ulc-bg-code); color: var(--ulc-text-code); 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 var(--ulc-border-primary); 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: attr(data-locale); display: block; width: 100%; text-align: center; color: var(--ulc-text-placeholder); font-size: 14px; padding: 20px; }
        ${containerID} .ulc-list-transform { position: relative; max-height: 100px; }
        ${containerID} .ulc-list-transform::before { content: attr(data-locale); display: inline-block; background: var(--ulc-bg-primary); position: absolute; top: -10px; left: 10px; padding: 0 5px; font-size: 12px; color: var(--ulc-text-tertiary); }
        ${containerID} .ulc-list-transform .ulc-list-transform-content { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px; border-top: 1px solid var(--ulc-border-primary); flex-shrink: 0; overflow-y: auto; height: 100%; }
        ${containerID} .ulc-list-transform .ulc-list-transform-content > span { display: inline-block; background: var(--ulc-bg-param-transform); color: var(--ulc-text-param-transform); padding: 3px 6px; border-radius: 3px; margin: 0; font-size: 14px; }
        ${containerID} .ulc-param { display: inline-flex; align-items: center; background: var(--ulc-bg-param); color: var(--ulc-text-param); padding: 5px 10px; border-radius: 6px; margin: 5px; font-size: 14px; }
        ${containerID} .ulc-param span { display: inline; margin-right: 8px; }
        ${containerID} .ulc-delete { color: var(--ulc-text-delete-icon); cursor: pointer; font-weight: bold; font-size: 16px; line-height: 1; padding: 4px 8px; margin: -4px -8px; border-radius: 6px; }
        ${containerID} .ulc-rule-settings-footer { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-top: 1px solid var(--ulc-border-primary); 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 #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 var(--ulc-border-primary); justify-content: flex-end; gap: 10px; flex-shrink: 0; }
        ${containerID} .ulc-form-hint { display: block; font-size: 12px; color: var(--ulc-text-secondary); 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: var(--ulc-bg-code-hint); color: var(--ulc-text-code); 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; pointer-events: none; }
        ${containerID} #ulc-toast.show { opacity: 1; visibility: visible; }
        ${containerID} .ulc-tabs, ${containerID} .ulc-list, ${containerID} .ulc-form-content, ${containerID} .ulc-list-transform .ulc-list-transform-content, ${containerID} textarea { overscroll-behavior: contain; }
        ${containerID} .ulc-confirming-action { background-color: var(--ulc-bg-danger-btn-hover) !important; border-color: var(--ulc-border-danger-btn) !important; color: var(--ulc-text-danger-btn-hover) !important; }
        ${containerID} .ulc-confirming-action:hover { background-color: #e60000 !important; }
        ${containerID}.theme-dark .ulc-confirming-action:hover { background-color: #e60000 !important; }
        ${containerID} .ulc-confirming-action, ${containerID} .ulc-confirmation-activating { position: relative; }
        ${containerID} .ulc-confirming-action::before { content: attr(data-locale); position: absolute; bottom: 100%; right: 0;  margin-bottom: 8px; background: var(--ulc-bg-confirm-tooltip); 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::after { content: ''; position: absolute; bottom: 100%; right: 50%; transform: translateX(-50%); margin-bottom: -4px; border: 6px solid transparent; border-top-color: var(--ulc-bg-confirm-tooltip); z-index: 1; pointer-events: none; }
        ${containerID} .ulc-confirmation-activating { cursor: wait; animation: ulc-pulse 0.5s ease-out; }
        ${containerID} .ulc-footer-switches { display: flex; align-items: center; gap: 20px; }
        ${containerID} .ulc-switch-container { display: flex; align-items: center; cursor: pointer; font-size: 13px; color: var(--ulc-text-secondary); }
        ${containerID} .ulc-switch-label { margin-right: 8px; }
        ${containerID} .ulc-switch-container input[type="checkbox"] { all: unset; box-sizing: border-box; appearance: none; -webkit-appearance: none; position: relative; width: 38px; height: 20px; border-radius: 10px; background-color: var(--ulc-border-secondary); transition: background-color 0.2s; flex-shrink: 0; border: none; margin: 0; }
        ${containerID} .ulc-switch-container input[type="checkbox"]::after { all: unset; box-sizing: border-box; content: ''; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; border-radius: 50%; background-color: white; transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); border-width: 0; transform: none; }
        ${containerID} .ulc-switch-container input[type="checkbox"]:checked { background-color: var(--ulc-accent-static); border-color: transparent; }
        ${containerID} .ulc-switch-container input[type="checkbox"]:checked::after { transform: translateX(18px); }
        @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); } }
        :is(${containerID} .ulc-tabs, ${containerID} .ulc-list, ${containerID} .ulc-form-content, ${containerID} textarea, ${containerID} .ulc-list-transform .ulc-list-transform-content)::-webkit-scrollbar { width: 10px; height: 10px; }
        :is(${containerID} .ulc-tabs, ${containerID} .ulc-list, ${containerID} .ulc-form-content, ${containerID} textarea, ${containerID} .ulc-list-transform .ulc-list-transform-content)::-webkit-scrollbar-track { background: var(--ulc-scrollbar-bg); }
        :is(${containerID} .ulc-tabs, ${containerID} .ulc-list, ${containerID} .ulc-form-content, ${containerID} textarea, ${containerID} .ulc-list-transform .ulc-list-transform-content)::-webkit-scrollbar-thumb { background-color: var(--ulc-scrollbar-thumb-bg); border-radius: 5px; border: 2px solid var(--ulc-scrollbar-bg); }
        :is(${containerID} .ulc-tabs, ${containerID} .ulc-list, ${containerID} .ulc-form-content, ${containerID} textarea, ${containerID} .ulc-list-transform .ulc-list-transform-content)::-webkit-scrollbar-thumb:hover { background-color: var(--ulc-scrollbar-thumb-hover-bg); }
        @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 var(--ulc-border-primary); flex-shrink: 0; padding: 12px 12px 0; gap: 10px; }
          ${containerID} .ulc-search-container { order: 1; flex-grow: 1; padding: 0; }
          ${containerID} .ulc-search-container input[type="search"] { font-size: 15px; padding: 10px 12px; }
          ${containerID} #ulc-add-rule-btn { order: 2; flex-shrink: 0; padding: 0; margin: 0; border: 1px solid var(--ulc-border-mobile-add-btn); font-size: 0; line-height: 1; background: var(--ulc-bg-mobile-add-btn); 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 var(--ulc-border-primary); scrollbar-width: none; -ms-overflow-style: none; }
          ${containerID} .ulc-tabs::-webkit-scrollbar { display: none; }
          ${containerID} .ulc-tab { display: flex; justify-content: center; align-items: center; width: 100px; height: 40px; padding: 0 15px; border-left: 0; border-bottom: 3px solid transparent; font-size: 15px; flex-shrink: 0; contain: strict; content-visibility: auto; contain-intrinsic-size: 100px 40px; min-width: 0; }
          ${containerID} .ulc-tab::before { display: none; }
          ${containerID} .ulc-tab.active { border-bottom-color: var(--ulc-accent-static); color: var(--ulc-accent-static); 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%; }
        }
        @media (hover: hover) {
            ${containerID} button.ulc-btn-primary:hover { background-color: var(--ulc-accent-hover); }
            ${containerID} button.ulc-btn-secondary:hover { background-color: var(--ulc-bg-secondary-btn-hover); }
            ${containerID} button.ulc-btn-danger:hover { background-color: var(--ulc-bg-danger-btn-hover); color: var(--ulc-text-danger-btn-hover); }
            ${containerID} .ulc-tab:hover { background: var(--ulc-bg-tab-hover); }
            ${containerID} #ulc-add-rule-btn:hover { background: var(--ulc-bg-add-rule-btn-hover); }
            ${containerID} #ulc-close-btn:hover { color: var(--ulc-text-close-btn-hover); }
            ${containerID} .ulc-delete:hover { color: var(--ulc-text-delete-icon-hover); }
        }
      `);
    }
  };

  const CodeInjector = {
    injectedCode: function (injectedConfig) {
      (() => {
        const { IS_DEBUG, PANEL_ID, locale, sandboxConfig, isFallbackMode = false, sandboxUnsafeWindow } = injectedConfig;
        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}`; },
            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); },
          warnLine(...args) { this._createLog('WARN', 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 = {
          // 翻译函数
          t(key, replacements = {}) {
            const path = key.split('.');
            let result = locale;
            for (const p of path) {
              result = result?.[p];
              if (result === undefined) {
                Logger.warnLine(`[i18n] Missing translation for key: ${key}`);
                return `[${key}]`;
              }
            }

            if (typeof result !== 'string') {
              return `[INVALID_KEY: ${key}]`;
            }

            return result.replace(/\{(\w+)\}/g, (match, RKey) => {
              return replacements[RKey] !== undefined ? String(replacements[RKey]) : match;
            });
          },

          escapeHTML(str) {
            if (typeof str !== 'string') return '';
            return str.replace(/[&<>"']/g, match => {
              switch (match) {
                case '&': return '&amp;';
                case '<': return '&lt;';
                case '>': return '&gt;';
                case '"': return '&quot;';
                case "'": return '&#039;';
              }
            });
          },

          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 {
              const encoder = new TextEncoder();
              const data = encoder.encode(name);
              const binaryString = String.fromCodePoint(...data);
              let base64 = btoa(binaryString);
              base64 = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

              return `rule-${base64}`;
            } catch (e) {
              Logger.error(`Failed to generate a safe ID for rule name: ${name}`, e);
              return `rule-error-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
            }
          },

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

          // 规范化配置对象
          _normalizeAndValidate(configObject, performValidation = false) {
            if (typeof configObject !== 'object' || configObject === null || Array.isArray(configObject)) {
              if (IS_DEBUG) Logger.warn('Invalid top-level config format. Expected an object.', { received: configObject });
              const errorResult = { error: '配置顶层必须是一个对象。' };
              const safeConfig = { config: { general: { params: [] }, rules: [] } };
              return performValidation ? errorResult : safeConfig;
            }

            let newConfig;
            try {
              newConfig = structuredClone(configObject);
            } catch (e) {
              const errorMsg = `配置无法被复制,可能包含无效内容(如函数)。错误: ${e.message}`;
              if (IS_DEBUG) Logger.error(errorMsg, { received: configObject });
              return { error: errorMsg };
            }

            const validationErrors = [];
            const ruleNames = new Set();

            if (typeof newConfig.general !== 'object' || newConfig.general === null || Array.isArray(newConfig.general)) {
              newConfig.general = { params: [] };
            }
            if (!Array.isArray(newConfig.general.params)) {
              newConfig.general.params = [];
            }

            newConfig.general.params = newConfig.general.params.filter(p => typeof p === 'string' && p.trim()).map(p => p.trim());

            if (!Array.isArray(newConfig.rules)) {
              newConfig.rules = [];
            }
            newConfig.rules = newConfig.rules.map((rule, index) => {
              if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {
                if (performValidation) validationErrors.push(`规则 #${index + 1} 不是一个有效的对象,已被忽略。`);
                return null;
              }

              rule.name = (typeof rule.name === 'string') ? rule.name.trim() : '';
              const rawMatch = Array.isArray(rule.match) ? rule.match : (typeof rule.match === 'string' ? [rule.match] : []);
              rule.match = rawMatch.filter(m => typeof m === 'string' && m.trim()).map(m => m.trim());

              rule.params = Array.isArray(rule.params) ? rule.params.filter(p => typeof p === 'string' && p.trim()).map(p => p.trim()) : [];
              rule.transform = Array.isArray(rule.transform) ? rule.transform.filter(t => typeof t === 'string' && t.trim()).map(t => t.trim()) : [];

              rule.enabled = rule.enabled !== false;
              rule.applyGeneral = rule.applyGeneral !== false;

              if (performValidation) {
                if (!rule.name) {
                  validationErrors.push(`规则 #${index + 1} (匿名) 缺少有效的名称。`);
                } else {
                  const lowerCaseName = rule.name.toLowerCase();
                  if (lowerCaseName === 'general') {
                    validationErrors.push(`规则名称 "${rule.name}" 是保留字。`);
                  } else if (ruleNames.has(lowerCaseName)) {
                    validationErrors.push(`配置中存在重复的规则名称: "${rule.name}"`);
                  }
                  ruleNames.add(lowerCaseName);
                }
                if (rule.match.length === 0) {
                  validationErrors.push(`规则 "${rule.name || `#${index + 1}`}" 缺少有效的匹配地址。`);
                }
              }

              return rule;
            }).filter(Boolean);

            if (performValidation && validationErrors.length > 0) {
              return { error: validationErrors.join('\n') };
            }

            return { config: newConfig };
          },

          validateAndNormalizeConfig(configObject) {
            return this._normalizeAndValidate(configObject, true);
          },

          normalizeConfig(configObject) {
            return this._normalizeAndValidate(configObject, false).config;
          },
        };

        // --- State (状态管理) ---
        const State = {
          config: null,
          DEFAULT_CONFIG: null,
          ruleCache: new Map(),
          cleanedAttrName: '',
          invalidAttrName: '',
          ui: {
            activeTab: GENERAL_TAB_ID,
            activeRuleIndex: -1,
            view: 'list', // 'list', 'add', 'edit', 'config-text'
            searchQuery: '',
            ACTIVATION_DELAY: 600,  // confirmation activation delay
            isDarkMode: false,
          },
          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 = {
          MAX_RECURSION_DEPTH: 5,

          saveConfig() {
            window.dispatchEvent(new CustomEvent('ulc-save-config', { detail: State.config }));
            State.ruleCache.clear();
            Logger.log('[Cache] Rules cache cleared due to local configuration change.');
          },

          // 计算最终规则集
          _calculateFinalRules(absoluteUrl, relevantRules, generalConfig) {
            const finalParams = new Set();
            const finalTransforms = new Set();
            const matchingRules = [];

            // 精确匹配
            for (const rule of relevantRules) {
              if (!rule || !rule.transform) continue;
              if (!rule.enabled) continue;
              for (const match of rule.match) {
                if (Utils.wildcardToRegex(match).test(absoluteUrl.href)) {
                  matchingRules.push(rule);
                  break;
                }
              }
            }

            // 规则合并
            if (matchingRules.length > 0) {
              let shouldApplyGeneral = false;
              matchingRules.forEach(rule => {
                rule.params.forEach(p => finalParams.add(p));
                rule.transform.forEach(t => finalTransforms.add(t));
                if (rule.applyGeneral) {
                  shouldApplyGeneral = true;
                }
              });
              if (shouldApplyGeneral) {
                (generalConfig.params || []).forEach(p => finalParams.add(p));
              }
            } else {
              (generalConfig.params || []).forEach(p => finalParams.add(p));
            }

            return { params: finalParams, transforms: finalTransforms };
          },

          // 获取与URL相关的规则
          getRulesForUrl(urlString) {
            let hostname;
            let absoluteUrl;
            try {
              absoluteUrl = new URL(urlString, window.location.href);
              hostname = absoluteUrl.hostname;
            } catch (e) {
              return { params: new Set(), transforms: new Set() };
            }

            let relevantRules;
            if (State.ruleCache.has(hostname)) {
              relevantRules = State.ruleCache.get(hostname);
            } else {
              relevantRules = State.config.rules.filter(rule =>
                rule.match.some(matchPattern => {
                  const domain = (matchPattern.split('://')[1] || matchPattern).split('/')[0].replace(/\*|^\./g, '');
                  return hostname.endsWith(domain);
                })
              );
              State.ruleCache.set(hostname, relevantRules);
            }

            return this._calculateFinalRules(absoluteUrl, relevantRules, State.config.general);
          },

          cleanUrl(urlString, recursionDepth = 0) {
            if (recursionDepth > this.MAX_RECURSION_DEPTH) {
              Logger.warnLine(`[Core] Max recursion depth reached for URL: ${urlString}`);
              return urlString;
            }
            if (!urlString || typeof urlString !== 'string') return urlString;

            const originalUrlString = urlString;

            let urlObject;
            try {
              urlObject = new URL(originalUrlString, window.location.href);
            } catch (e) {
              return originalUrlString;
            }

            const { params: paramsToRemove, transforms: transformKeysToUse } = this.getRulesForUrl(urlObject.href);

            if (transformKeysToUse.size > 0) {
              if (urlObject.searchParams.size === 1) {
                const weirdUrl = Utils.extractUrlFromWeirdParam(urlObject.href);
                if (weirdUrl) return this.cleanUrl(weirdUrl, recursionDepth + 1);
              }
              for (const key of transformKeysToUse) {
                if (urlObject.searchParams.has(key)) {
                  const value = urlObject.searchParams.get(key);
                  const transformedUrl = Utils.tryAllDecodes(value);
                  if (transformedUrl) {
                    Logger.log('Link transformed based on a specific rule:', { from: originalUrlString, to: transformedUrl });
                    return this.cleanUrl(transformedUrl, recursionDepth + 1);
                  }
                }
              }
            }

            let modified = false;

            if (paramsToRemove.size > 0) {
              const paramsToDelete = [];
              for (const key of urlObject.searchParams.keys()) {
                if (paramsToRemove.has(key)) {
                  paramsToDelete.push(key);
                }
              }
              if (paramsToDelete.length > 0) {
                paramsToDelete.forEach(key => urlObject.searchParams.delete(key));
                modified = true;
              }
            }

            for (const [key, value] of urlObject.searchParams.entries()) {
              try {
                const decodedValue = decodeURIComponent(value);
                if (Utils.isValidAbsoluteURL(decodedValue)) {
                  const cleanedInnerUrl = this.cleanUrl(decodedValue, recursionDepth + 1);
                  if (cleanedInnerUrl !== decodedValue) {
                    urlObject.searchParams.set(key, encodeURIComponent(cleanedInnerUrl));
                    modified = true;
                    Logger.log(`Nested URL in parameter "${key}" purified.`, { from: decodedValue, to: cleanedInnerUrl });
                  }
                }
              } catch (e) { /* Silently ignore decoding errors */ }
            }

            if (!modified) return originalUrlString;

            const isOriginalRelative = !/^(https?:)?\/\//.test(originalUrlString);
            if (isOriginalRelative) {
              return urlObject.pathname + urlObject.search + urlObject.hash;
            } else {
              return urlObject.href;
            }
          },

        };

        // --- UI (界面渲染) ---
        const UI = {
          _policy: null,
          _currentDetailView: null,

          setSafelyInnerHTML(element, htmlString) {
            if (this._policy === null) {
              this._policy = false;

              if (window.trustedTypes && window.trustedTypes.createPolicy) {
                try {
                  this._policy = window.trustedTypes.createPolicy('URLCleanerPolicy#html', {
                    createHTML: s => s,
                  });
                } catch (e) {
                  if (window.trustedTypes.defaultPolicy) {
                    this._policy = window.trustedTypes.defaultPolicy;
                    Logger.log('Using host page default Trusted Types policy as a fallback.');
                  }
                }
              }
            }

            if (this._policy) {
              try {
                element.innerHTML = this._policy.createHTML(htmlString);
              } catch (e) {
                Logger.error('UI Rendering failed even with a Trusted Types policy.', `Policy in use: ${this._policy.name}`, `Error: ${e.message}`);
              }
            } else {
              try {
                element.innerHTML = htmlString;
              } catch (e) {
                Logger.error('UI Rendering blocked by CSP.', `Error: ${e.message}`);
              }
            }
          },

          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.action = 'openTab';
            generalTab.dataset.tabId = GENERAL_TAB_ID;
            generalTab.dataset.ruleIndex = '-1';
            generalTab.textContent = Utils.t('ui.tabs.general');
            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.action = 'openTab';
              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="${Utils.t('ui.placeholders.search')}">
              </div>
              <ul class="ulc-tabs"></ul>
              <div id="ulc-add-rule-btn" data-action="openAddRuleForm" data-locale=" ${Utils.escapeHTML(Utils.t('ui.buttons.addRule'))}">+</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();
          },

          _createDetailView() {
            const mainContentContainer = State.dom.mainContentContainer;
            mainContentContainer.innerHTML = '';

            const template = `
                <div class="ulc-header">
                    <div class="ulc-title-container">
                        <h3></h3>
                    </div>
                    <button data-action="closePanel" id="ulc-close-btn">×</button>
                </div>
                <div class="ulc-sub-header">
                    <span>${Utils.t('ui.labels.matchAddressShort')}:</span>
                    <div class="ulc-match-tags"></div>
                </div>
                <div class="ulc-add">
                    <input type="text" id="ulc-new-param" placeholder="${Utils.t('ui.placeholders.addParam')}"/>
                    <button id="ulc-add-btn" data-action="addParam" class="ulc-btn-primary">${Utils.t('ui.buttons.add')}</button>
                </div>
                <div class="ulc-list" data-locale="${Utils.escapeHTML(Utils.t('ui.misc.noParams'))}"></div>
                <div class="ulc-list-transform" data-locale="${Utils.escapeHTML(Utils.t('ui.misc.transformTitle'))}">
                    <div class="ulc-list-transform-content"></div>
                </div>
                <div class="ulc-rule-settings-footer"></div>
            `;
            this.setSafelyInnerHTML(mainContentContainer, template);

            this._currentDetailView = {
              container: mainContentContainer,
              titleContainer: mainContentContainer.querySelector('.ulc-title-container'),
              title: mainContentContainer.querySelector('h3'),
              subHeader: mainContentContainer.querySelector('.ulc-sub-header'),
              matchTags: mainContentContainer.querySelector('.ulc-match-tags'),
              paramsList: mainContentContainer.querySelector('.ulc-list'),
              transformContainer: mainContentContainer.querySelector('.ulc-list-transform'),
              transformContent: mainContentContainer.querySelector('.ulc-list-transform-content'),
              footer: mainContentContainer.querySelector('.ulc-rule-settings-footer'),
            };
          },

          _updateDetailView() {
            const view = this._currentDetailView;
            if (!view) return;

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

            const params = isGeneral ? State.config.general.params : (rule.params || []);
            const transform = isGeneral ? [] : (rule.transform || []);
            const titleText = isGeneral ? Utils.t('ui.titles.generalList') : rule.name;

            view.titleContainer.querySelector('.ulc-edit-icon')?.remove();
            view.matchTags.innerHTML = '';
            view.paramsList.innerHTML = '';
            view.transformContent.innerHTML = '';
            view.footer.innerHTML = '';

            view.title.textContent = titleText;
            view.title.title = titleText;

            if (isGeneral) {
              view.subHeader.style.display = 'none';
              const footerHtml = `
              <div><button id="ulc-config-text-btn" data-action="openConfigText" class="ulc-btn-secondary">${Utils.t('ui.buttons.configText')}</button></div>
              <button id="ulc-reset-btn" data-action="resetGeneral" class="ulc-btn-secondary" data-locale="${Utils.escapeHTML(Utils.t('prompts.resetGeneral'))}">${Utils.t('ui.buttons.reset')}</button>`;
              this.setSafelyInnerHTML(view.footer, footerHtml);
            } else {
              view.subHeader.style.display = 'flex';

              const editIconSvg = `<svg data-action="openEditRuleForm" 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>`;
              view.title.insertAdjacentHTML('afterend', editIconSvg);

              rule.match.forEach(m => {
                const codeEl = document.createElement('code');
                codeEl.textContent = m;
                view.matchTags.appendChild(codeEl);
              });

              const footerHtml = `
              <div class="ulc-footer-switches">
                  <label class="ulc-switch-container">
                      <span class="ulc-switch-label">${Utils.t('ui.labels.enableRule')}</span>
                      <input type="checkbox" data-action="toggleRuleEnabled">
                  </label>
                  <label class="ulc-switch-container">
                      <span class="ulc-switch-label">${Utils.t('ui.labels.applyGeneral')}</span>
                      <input type="checkbox" data-action="toggleApplyGeneral">
                  </label>
              </div>
              <button id="ulc-delete-rule-btn" data-action="deleteRule" class="ulc-btn-danger" data-locale="${Utils.escapeHTML(Utils.t('prompts.deleteRule', { ruleName: rule.name }))}">${Utils.t('ui.buttons.delete')}</button>`;
              this.setSafelyInnerHTML(view.footer, footerHtml);
              view.footer.querySelector('[data-action="toggleRuleEnabled"]').checked = rule.enabled;
              view.footer.querySelector('[data-action="toggleApplyGeneral"]').checked = rule.applyGeneral;
            }

            [...params].sort().forEach(p => {
              const paramEl = document.createElement('div');
              paramEl.className = 'ulc-param';
              const span = document.createElement('span');
              span.textContent = p;
              const del = document.createElement('div');
              del.className = 'ulc-delete';
              del.textContent = '×';
              del.dataset.action = 'deleteParam';
              del.dataset.param = p;
              paramEl.append(span, del);
              view.paramsList.appendChild(paramEl);
            });

            view.transformContainer.style.display = transform.length > 0 ? 'block' : 'none';
            if (transform.length > 0) {
              transform.forEach(t => {
                const span = document.createElement('span');
                span.textContent = t;
                view.transformContent.appendChild(span);
              });
            }
          },

          renderMainContent() {
            if (!State.dom.mainContentContainer) return;

            if (State.ui.view === 'list') {
              if (!this._currentDetailView || !this._currentDetailView.container.isConnected) {
                this._createDetailView();
              }
              this._updateDetailView();
            } else {
              this._currentDetailView = null;

              State.dom.mainContentContainer.innerHTML = '';
              let contentHtml = '';
              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();
          },

          renderRuleForm() {
            const isEdit = State.ui.view === 'edit';
            const rule = isEdit ? State.config.rules[State.ui.activeRuleIndex] : null;
            const title = isEdit ? Utils.t('ui.titles.editRule') : Utils.t('ui.titles.addRule');
            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) { Logger.warn('Failed to get default rule name from hostname.', e.message); }
            }
            return `
              <div class="ulc-main-content">
                  <div class="ulc-header"><h3>${title}</h3></div>
                  <div class="ulc-form-content">
                  <label for="ulc-rule-name">${Utils.t('ui.labels.ruleName')}</label>
                  <input type="text" id="ulc-rule-name" placeholder="${Utils.t('ui.placeholders.ruleName')}" maxlength="30" value="${ruleName}">
                  <label for="ulc-rule-match">${Utils.t('ui.labels.matchAddress')}</label>
                  <textarea id="ulc-rule-match" placeholder="${Utils.t('ui.placeholders.matchAddress')}">${matchPatterns}</textarea>
                  <div class="ulc-form-hint">${Utils.t('ui.hints.matchAddress')}</div>
                  <label for="ulc-transform-keys">${Utils.t('ui.labels.transformKeys')}</label>
                  <textarea id="ulc-transform-keys" placeholder="${Utils.t('ui.placeholders.transformKeys')}">${transformKeys}</textarea>
                  <p>${Utils.t('ui.hints.transform')}</p>
                  </div>
                  <div class="ulc-form-actions">
                  <button id="ulc-cancel-add-rule-btn" data-action="cancelRuleForm" class="ulc-btn-secondary">${Utils.t('ui.buttons.cancel')}</button>
                  <button id="ulc-save-rule-btn" data-action="saveRule" class="ulc-btn-primary">${Utils.t('ui.buttons.saveRule')}</button>
                  </div>
              </div>`;
          },

          renderConfigTextForm() {
            const cleanConfig = structuredClone(State.config);
            cleanConfig.rules.forEach(rule => {
              if (rule.params && rule.params.length === 0) { delete rule.params; }
              if (rule.transform && rule.transform.length === 0) { delete rule.transform; }
              if (rule.enabled === true) { delete rule.enabled; }
              if (rule.applyGeneral === true) { delete rule.applyGeneral; }
            });

            const configString = JSON.stringify(cleanConfig, null, 2);

            return `
            <div class="ulc-main-content">
                <div class="ulc-header"><h3>${Utils.t('ui.titles.configText')}</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" data-action="cancelConfigText" class="ulc-btn-secondary">${Utils.t('ui.buttons.cancel')}</button>
                <button id="ulc-save-config-text-btn" data-action="saveConfigText" class="ulc-btn-primary">${Utils.t('ui.buttons.save')}</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 = Utils.t('ui.buttons.confirm')) {
            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(Utils.t('toasts.extracted', { count: 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 {
              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(Utils.t('toasts.allEmpty'));
              return false;
            }
            if (newName.toLowerCase() === GENERAL_TAB_ID) {
              UI.showToast(Utils.t('toasts.nameReserved'));
              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(Utils.t('toasts.nameExists'));
              return false;
            }
            const ruleData = { name: newName, match: newMatches };
            if (isEdit) {
              const rule = State.config.rules[ruleIndex];
              Object.assign(rule, ruleData);
              rule.transform = newTransformKeys;
            } else {
              const newRule = {
                ...ruleData,
                params: [],
                transform: newTransformKeys,
                enabled: true,
                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(Utils.t('toasts.ruleSaved', { ruleName: 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(Utils.t('toasts.ruleDeleted'));
            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(Utils.t('toasts.jsonInvalid', { error: e.message }), 3000);
              return false;
            }

            const result = Utils.validateAndNormalizeConfig(newConfig);
            if (result.error) {
              Logger.warn('Save from text failed: Invalid configuration.', { error: result.error, config: newConfig });
              UI.showToast(Utils.t('toasts.configInvalid', { error: result.error }), 4000);
              return false;
            }

            const normalizedConfig = result.config;

            State.config.general = normalizedConfig.general;
            State.config.rules = normalizedConfig.rules;

            UI.showToast(Utils.t('toasts.configSaved'));
            return true;
          },

          resetConfig() {
            State.config.general.params = structuredClone(State.DEFAULT_CONFIG.general.params);
            Logger.warn('General rules have been reset to default.');
            UI.showToast(Utils.t('toasts.configReset'));
            return true;
          },

          onSearchInputDebounced: null, // 防抖处理的 input 事件处理器
          _performSearch(query) {
            if (query !== State.ui.searchQuery) {
              State.ui.searchQuery = query;
              UI.updateRuleList();
            }
          },

          // 动作字典
          actions: {
            closePanel() {
              if (State.dom.settingsPanel) {
                State.dom.settingsPanel.remove();
                State.dom.settingsPanel = null;
              }
            },
            openTab(target) {
              const tabId = target.dataset.tabId;
              if (State.ui.activeTab === tabId) return;

              State.ui.activeTab = tabId;
              State.ui.activeRuleIndex = parseInt(target.dataset.ruleIndex, 10);
              State.ui.view = 'list';

              UI.renderPanel();
            },
            addParam() {
              if (Events.addParamsFromInput()) {
                Core.saveConfig();
                UI.renderMainContent();
              }
            },
            deleteParam(target) {
              if (Events.deleteParam(target.dataset.param)) {
                Core.saveConfig();
                UI.renderMainContent();
              }
            },
            openAddRuleForm() {
              State.ui.view = 'add';
              UI.renderMainContent();
            },
            openEditRuleForm() {
              State.ui.view = 'edit';
              UI.renderMainContent();
            },
            deleteRule(target) {
              Events.requestConfirmation(target, () => {
                const originalIndex = State.ui.activeRuleIndex;
                if (Events.deleteCurrentRule()) {
                  const newIndex = originalIndex - 1;

                  if (State.config.rules.length === 0) {
                    State.ui.activeRuleIndex = -1;
                    State.ui.activeTab = GENERAL_TAB_ID;
                  } else {
                    const newSafeIndex = Math.max(-1, Math.min(newIndex, State.config.rules.length - 1));
                    State.ui.activeRuleIndex = newSafeIndex;
                    if (newSafeIndex === -1) {
                      State.ui.activeTab = GENERAL_TAB_ID;
                    } else {
                      const newActiveRule = State.config.rules[newSafeIndex];
                      State.ui.activeTab = Utils.getRuleTabId(newActiveRule);
                    }
                  }
                  Core.saveConfig();
                  UI.renderPanel();
                }
              }, Utils.t('ui.buttons.confirmDelete'));
            },
            resetGeneral(target) {
              Events.requestConfirmation(target, () => {
                if (Events.resetConfig()) {
                  Core.saveConfig();
                  UI.renderMainContent();
                }
              }, Utils.t('ui.buttons.confirmReset'));
            },
            toggleRuleEnabled(target) {
              const rule = State.config.rules[State.ui.activeRuleIndex];
              if (rule) rule.enabled = target.checked;
              Core.saveConfig();
            },
            toggleApplyGeneral(target) {
              const rule = State.config.rules[State.ui.activeRuleIndex];
              if (rule) rule.applyGeneral = target.checked;
              Core.saveConfig();
            },
            saveRule() {
              if (Events.saveRule()) {
                State.ui.view = 'list';
                Core.saveConfig();
                UI.renderPanel();
              }
            },
            cancelRuleForm() {
              State.ui.view = 'list';
              UI.renderMainContent();
            },
            openConfigText() {
              State.ui.view = 'config-text';
              UI.renderMainContent();
            },
            saveConfigText() {
              if (Events.saveConfigFromText()) {
                State.ui.view = 'list';
                State.ui.activeTab = GENERAL_TAB_ID;
                State.ui.activeRuleIndex = -1;
                Core.saveConfig();
                UI.renderPanel();
              }
            },
            cancelConfigText() {
              State.ui.view = 'list';
              UI.renderMainContent();
            }
          },

          // 核心事件处理器
          handlePanelClick(e) {
            const target = e.target;
            const actionTarget = target.closest('[data-action]');

            if (!actionTarget) {
              if (this._confirmingAction.el) {
                this._resetConfirmation();
              }
              return;
            }

            if (this._confirmingAction.el === actionTarget) {
              if (this._confirmingAction.isActivating) {
                return;
              } else {
                this._confirmingAction.onConfirm();
                this._resetConfirmation();
                return;
              }
            }

            if (this._confirmingAction.el) {
              this._resetConfirmation();
            }

            const action = actionTarget.dataset.action;
            if (this.actions[action]) {
              this.actions[action](actionTarget, e);
            }
          },

          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 => {
              if (e.target.closest(`#${State.dom.panelId}`)) return;
              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
                  });
                }

                return original.apply(this, [state, title, cleanedUrl, ...rest]);
              };
            };

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

            // 沙盒模式下window.open无法正确拦截,所以需要使用unsafeWindow
            const openContext = isFallbackMode ? sandboxUnsafeWindow : window;
            if (openContext) {
              const originalOpen = openContext.open;
              if (typeof originalOpen === 'function') {
                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.isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
                if (State.ui.isDarkMode) {
                  State.dom.settingsPanel.classList.add('theme-dark');
                }
                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 newRawConfig = event.detail;

              // 更新配置
              State.config = newRawConfig;
              State.ruleCache.clear();
              Logger.log('[Cache] Rules cache cleared due to cross-tab sync.');

              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 normalizedConfig = Utils.normalizeConfig(sandboxConfig.config);
          const normalizedDefaultConfig = Utils.normalizeConfig(sandboxConfig.defaultConfig);
          State.init(normalizedConfig, normalizedDefaultConfig);

          Events.onSearchInputDebounced = Utils.debounce((e) => {
            Events._performSearch(e.target.value);
          }, 250);

          State.cleanedAttrName = Utils.randomString();
          State.invalidAttrName = Utils.randomString();

          const cleanedPageUrl = Core.cleanUrl(window.location.href);
          if (window.location.href !== cleanedPageUrl) {
            history.replaceState(history.state, '', cleanedPageUrl);
          }
          Events.handlePanelClick = Events.handlePanelClick.bind(Events);
          Events.initEventListeners();
        }

        main();

      })();
    },

    inject(injectedConfig = {}) {
      const nonce = document.querySelector('script[nonce]')?.nonce || document.querySelector('style[nonce]')?.nonce;
      const injectedScript = document.createElement('script');
      injectedScript.id = 'ulc-injected-script';
      injectedScript.nonce = nonce;
      const scriptContent = `
            ((injectedCode) => {
                const injectedOptions = ${JSON.stringify(injectedConfig)};
                injectedCode(injectedOptions);
            })(${this.injectedCode.toString()});
        `;

      if (window.trustedTypes && window.trustedTypes.createPolicy) {
        try {
          const policy = window.trustedTypes.createPolicy('UniversalLinkCleanerPolicy', { createScript: s => s });
          injectedScript.textContent = policy.createScript(scriptContent);
        } catch (e) {
          injectedScript.textContent = scriptContent;
        }
      } else {
        injectedScript.textContent = scriptContent;
      }

      (document.head || document.documentElement).appendChild(injectedScript);
      injectedScript.remove();
    }
  };

  // --- 主执行流程 ---
  function main() {
    const PANEL_ID = 'p' + Math.random().toString(36).substring(2, 10);

    const activeLocale = I18nManager.getActiveLocale();

    Sandbox.loadConfig();
    Sandbox.init(activeLocale);
    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 = {
      sandboxConfig: {
        config: Sandbox.config,
        defaultConfig: Sandbox.DEFAULT_CONFIG
      },
      IS_DEBUG,
      PANEL_ID,
      locale: activeLocale,
    }

    // 注入模式
    CodeInjector.inject(INJECT_CONFIG);

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

  main();
})();