URLCleaner - Link Purifier & Tracker Remover

Automatically removes tracking parameters (e.g., fbclid, utm_source) from URLs and skips intermediate redirect pages. For a faster, safer, and cleaner browsing experience. Take back control of your links!

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name              URLCleaner - Link Purifier & Tracker Remover
// @name:zh-CN        链接净化 (URLCleaner) - 移除跟踪参数,直达目标页
// @name:zh-TW        鏈接淨化 (URLCleaner) - 移除跟蹤參數,直達目標頁
// @name:de           URLCleaner - Sicher & Schnell: Links bereinigen, Tracker entfernen
// @name:es           URLCleaner - Purificador de Enlaces y Eliminador de Rastreadores
// @name:fr           URLCleaner - Purificateur de Liens & Suppresseur de Traqueurs
// @name:ja           URLCleaner - リンク浄化 & トラッカー削除
// @name:ko           URLCleaner - 링크 정화 & 추적기 제거
// @name:ru           URLCleaner - Очиститель ссылок и удаление трекеров
// @name:pt           URLCleaner - Purificador de Links & Removedor de Rastreadores
// @namespace         You Boy
// @version           1.4.5
// @description       Automatically removes tracking parameters (e.g., fbclid, utm_source) from URLs and skips intermediate redirect pages. For a faster, safer, and cleaner browsing experience. Take back control of your links!
// @description:zh-CN 自动移除网址中的跟踪参数(如 spm_id_from, utm_source),并智能跳过中转页面。让你的每一次点击都更快、更安全。告别冗长网址,回归纯净浏览。
// @description:zh-TW 自動移除網址中的跟蹤參數(如 spm_id_from, utm_source),並智能跳過中轉頁面。讓你的每一次點擊都更快、更安全。告別冗長網址,回歸純淨瀏覽。
// @description:de    Bereinigt automatisch URLs von Tracking-Parametern (z.B. gclid, utm_source) und überspringt Zwischenseiten. Für schnelleres, sichereres und privates Surfen. Holen Sie sich die Kontrolle über Ihre Links zurück!
// @description:es    Elimina automáticamente los parámetros de seguimiento (p. ej., fbclid, utm_source) de las URL y omite las páginas de redirección intermedias. Para una navegación más rápida, segura y limpia.
// @description:fr    Supprime automatiquement les paramètres de suivi (par ex. fbclid, utm_source) des URL et contourne les pages de redirection intermédiaires. Pour une navigation plus rapide, plus sûre et plus propre.
// @description:ja    リンクの追跡パラメータ(例: utm_campaign, igshid)を自動で削除し、不要なリダイレクトページをスキップします。より速く、より安全なブラウジング体験を実現。長くて汚いURLから解放され、ストレスフリーなネットサーフィンを!
// @description:ko    URL에서 추적 매개변수(예: fbclid, utm_source)를 자동으로 제거하고 중간 리디렉션 페이지를 건너뜁니다. 더 빠르고, 안전하며, 깨끗한 브라우징 경험을 위해.
// @description:ru    Автоматически удаляет параметры отслеживания (например, fbclid, utm_source) из URL-адресов и пропускает промежуточные страницы перенаправления. Для более быстрого, безопасного и чистого просмотра веб-страниц.
// @description:pt    Remove automaticamente parâmetros de rastreamento (ex: fbclid, utm_source) dos URLs e pula páginas de redirecionamento intermediárias. Para uma navegação mais rápida, segura e limpa.
// @author            You Boy
// @match             *://*/*
// @exclude           *://localhost*
// @exclude           *://127.0.0.1*
// @exclude           *://192.168.*
// @grant             GM_addStyle
// @grant             GM_getValue
// @grant             GM_setValue
// @grant             GM_registerMenuCommand
// @grant             GM_addValueChangeListener
// @grant             unsafeWindow
// @run-at            document-start
// @license           MIT
// ==/UserScript==

(function () {
  "use strict";

  const IS_DEBUG = false;

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

  const 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: "启用规则",
            compatibilityMode: "兼容模式",
          },
          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:
              "部分网站跳转外链的时候会跳转到一个确认网页,配置参数会把对应参数内的外链直接转换为可点击链接。",
            compatibilityMode:
              "默认情况可能会导致某些网站功能异常,启用此功能可能有助于解决某些网站的点击兼容性问题。",
          },
          misc: {
            noParams: "未添加参数",
            transformTitle: "跳转参数",
          },
        },
        toasts: {
          paramsAdded: "成功添加 {count} 个新参数",
          paramsExist: "未添加新参数,因为它们已存在",
          allEmpty: "规则名称和匹配地址不能为空。",
          nameReserved: "错误:“general”是保留名称,请使用其他名称。",
          nameExists: "错误:已存在同名规则,请使用其他名称。",
          ruleSaved: "规则 “{ruleName}” 已保存",
          ruleDeleted: "已删除",
          configSaved: "配置已成功保存",
          configReset: "通用参数已重置为默认",
          jsonInvalid:
            "JSON 格式无效。请检查是否存在多余的逗号、缺失的括号等语法错误。\n技术错误: {error}",
          configInvalid: "配置内容验证失败:\n{error}",
          configNotAnObject: "配置文本必须是一个JSON对象。",
          configMissingRules: '配置对象必须包含一个名为 "rules" 的数组。',
          configInvalidContent:
            "配置中包含无法复制的内容,导入失败。\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",
            compatibilityMode: "Compatibility Mode",
          },
          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.",
            compatibilityMode:
              "The default setting may cause issues on some websites. Enabling this mode can help resolve click compatibility problems.",
          },
          misc: {
            noParams: "No parameters added",
            transformTitle: "Redirect Keys",
          },
        },
        toasts: {
          paramsAdded: "Successfully added {count} new parameter(s)",
          paramsExist: "No new parameters were added as they already exist.",
          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 for syntax errors like trailing commas or missing brackets.\nTechnical error: {error}",
          configInvalid: "Configuration content validation failed:\n{error}",
          configNotAnObject: "Configuration must be a JSON object.",
          configMissingRules:
            'Configuration object must provide a "rules" array.',
          configInvalidContent:
            "Configuration contains non-cloneable content and could not be imported.\nError: {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];
    },

    getDefaultLocale() {
      return this.locales[this.DEFAULT_LANG];
    },
  };

  // --- 沙箱环境 ---
  const Sandbox = {
    DEFAULT_CONFIG: {
      general: {
        params: [
          "_ga",
          "_hsenc",
          "_hsmi",
          "_ke",
          "dclid",
          "fbclid",
          "gclid",
          "igshid",
          "mc_cid",
          "mc_eid",
          "spm_id_from",
          "utm_campaign",
          "utm_content",
          "utm_medium",
          "utm_source",
          "utm_term",
        ],
      },
      rules: [],
    },

    config: null,

    loadConfig() {
      const configFromStorage = GM_getValue("ulcConfig");
      this.config = configFromStorage || structuredClone(this.DEFAULT_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;
          --ulc-bg-param-new: #adfdc1;
          --ulc-text-param-new: #555;
        }
        ${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;
          --ulc-bg-param-new: #3a4c58;
          --ulc-text-param-new: var(--ulc-text-primary);
        }
        ${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); } }
        ${containerID} .ulc-param-new { background-color: var(--ulc-bg-param-new) !important; color: var(--ulc-text-param-new) !important; }
        ${containerID} .ulc-param-new span::before { content: '✨'; display: inline-block; margin-right: 6px; }
        :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,
          defaultLocale,
          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(".");

            const findValueByPath = (obj, pathArray) => {
              let current = obj;
              for (const p of pathArray) {
                current = current?.[p];
                if (current === undefined) return undefined;
              }
              return current;
            };

            let result = findValueByPath(locale, path);

            if (result === undefined) {
              result = findValueByPath(defaultLocale, path);

              if (result === undefined) {
                Logger.warnLine(
                  `[i18n] Missing translation for key in ALL locales: ${key}`
                );
                return `[${key}]`;
              }
            }

            if (typeof result !== "string") {
              Logger.warnLine(
                `[i18n] Translation for key "${key}" is not a string, but a(n) "${typeof result}".`
              );
              return `[INVALID_KEY_TYPE: ${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;
                rule.compatibilityMode = !!rule.compatibilityMode;

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

          findLinkInEvent(event) {
            const closestLink = event.target.closest?.("a[href]");
            if (closestLink) {
              return closestLink;
            }

            const path = event.composedPath?.() || [];
            for (let i = 0; i < path.length; i++) {
              const element = path[i];
              if (element?.tagName === "A" && element.href) {
                return element;
              }
            }

            return null;
          },
        };

        // --- 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,
            highlightedParams: new Set(),
          },
          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) {
            for (const rule of relevantRules) {
              if (
                rule &&
                rule.enabled === false &&
                rule.applyGeneral === false
              ) {
                for (const match of rule.match) {
                  if (Utils.wildcardToRegex(match).test(absoluteUrl.href)) {
                    Logger.log(
                      `Cleaning explicitly disabled for ${absoluteUrl.href} by rule: "${rule.name}"`
                    );
                    return {
                      params: new Set(),
                      transforms: new Set(),
                      compatibilityMode: false,
                    };
                  }
                }
              }
            }

            const finalParams = new Set();
            const finalTransforms = new Set();
            let compatibilityMode;
            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) {
              compatibilityMode = false;
              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 (rule.compatibilityMode) {
                  compatibilityMode = true;
                }
              });
              if (shouldApplyGeneral) {
                (generalConfig.params || []).forEach((p) => finalParams.add(p));
              }
            } else {
              // 不存在任何特定规则,只应用通用规则
              (generalConfig.params || []).forEach((p) => finalParams.add(p));
              compatibilityMode = true; // 在这种情况下,默认值为 true (兼容模式)
            }

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

          // 获取与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(),
                compatibilityMode: false,
              };
            }

            if (State.ruleCache.has(hostname)) {
              return State.ruleCache.get(hostname);
            }

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

            const finalRuleSet = this._calculateFinalRules(
              absoluteUrl,
              relevantRules,
              State.config.general
            );
            State.ruleCache.set(hostname, finalRuleSet);
            return finalRuleSet;
          },

          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 highlightedParams = State.ui.highlightedParams;
            const view = this._currentDetailView;
            if (!view) return;

            const isGeneral = State.ui.activeTab === GENERAL_TAB_ID;
            const rule = isGeneral
              ? null
              : State.config.rules[State.ui.activeRuleIndex];

            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";
              if (highlightedParams.has(p)) {
                paramEl.classList.add("ulc-param-new");
              }
              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") {
              const isGeneral = State.ui.activeTab === GENERAL_TAB_ID;
              const rule = isGeneral
                ? null
                : State.config.rules[State.ui.activeRuleIndex];

              if (!isGeneral && !rule) {
                Logger.warnLine(
                  `Active rule not found, falling back to general view.`
                );
                State.ui.activeTab = GENERAL_TAB_ID;
                State.ui.activeRuleIndex = -1;
              }

              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 = "",
              compatibilityMode = false;
            if (isEdit && rule) {
              ruleName = rule.name;
              matchPatterns = rule.match.join("\n");
              transformKeys = Array.isArray(rule.transform)
                ? rule.transform.join("\n")
                : "";
              compatibilityMode = !!rule.compatibilityMode;
            } 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>
                  <label for="ulc-compatibility-mode">${Utils.t(
                    "ui.labels.compatibilityMode"
                  )}</label> 
                  <div class="ulc-switch-container" style="gap: 10px;">
                      <input type="checkbox" id="ulc-compatibility-mode" ${
                        compatibilityMode ? "checked" : ""
                      }>
                      <p>${Utils.t("ui.hints.compatibilityMode")}</p>
                  </div>
                  </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 configToExport = structuredClone(State.config);
            delete configToExport.general;

            if (Array.isArray(configToExport.rules)) {
              configToExport.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;
                }
                if (rule.compatibilityMode === false) {
                  delete rule.compatibilityMode;
                }
              });
            }

            const configString = JSON.stringify(configToExport, 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().replace(/['"]/g, "");
            let newParams = [];
            const parsedUrl = Utils.tryParseURL(inputValue);

            if (parsedUrl) {
              if (parsedUrl.searchParams.size === 0) {
                input.value = "";
                return false;
              }
              newParams = [...parsedUrl.searchParams.keys()];
            } 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);
            const addedParams = new Set();

            newParams.forEach((p) => {
              if (!paramsSet.has(p)) {
                paramsSet.add(p);
                addedParams.add(p);
              }
            });

            if (addedParams.size > 0) {
              const sortedParams = Array.from(paramsSet).sort();
              if (context.isGeneral) {
                State.config.general.params = sortedParams;
              } else {
                context.rule.params = sortedParams;
              }
              Logger.log(`Added ${addedParams.size} parameter(s)...`);
            }

            input.value = "";
            return addedParams;
          },

          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 compatibilityModeInput = document.getElementById(
              "ulc-compatibility-mode"
            );
            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)
              ),
            ];
            const newCompatibilityMode = compatibilityModeInput.checked;

            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,
              transform: newTransformKeys,
              compatibilityMode: newCompatibilityMode,
            };

            if (isEdit) {
              Object.assign(State.config.rules[ruleIndex], ruleData);
            } else {
              const newRule = {
                ...ruleData,
                params: [],
                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 parsedJson;
            try {
              parsedJson = JSON.parse(textarea.value);
            } catch (e) {
              UI.showToast(Utils.t("toasts.jsonInvalid", { error: e.message }));
              return false;
            }

            if (
              typeof parsedJson !== "object" ||
              parsedJson === null ||
              Array.isArray(parsedJson)
            ) {
              UI.showToast(Utils.t("toasts.configNotAnObject"));
              return false;
            }

            let newConfig;
            try {
              newConfig = structuredClone(parsedJson);
            } catch (e) {
              UI.showToast(
                Utils.t("toasts.configInvalidContent", { error: e.message })
              );
              return false;
            }

            newConfig.general = structuredClone(State.config.general);

            if (!Array.isArray(newConfig.rules)) {
              UI.showToast(Utils.t("toasts.configMissingRules"));
              return false;
            }

            const validationResult =
              Utils.validateAndNormalizeConfig(newConfig);

            if (validationResult.error) {
              UI.showToast(
                Utils.t("toasts.configInvalid", {
                  error: validationResult.error,
                })
              );
              return false;
            }

            State.config = validationResult.config;

            Core.saveConfig();
            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.highlightedParams.clear();
              State.ui.activeTab = tabId;
              State.ui.activeRuleIndex = parseInt(target.dataset.ruleIndex, 10);
              State.ui.view = "list";

              UI.renderPanel();
            },
            addParam() {
              const newlyAddedParams = Events.addParamsFromInput();
              if (newlyAddedParams === false) {
                return;
              }

              if (newlyAddedParams.size > 0) {
                UI.showToast(
                  Utils.t("toasts.paramsAdded", {
                    count: newlyAddedParams.size,
                  })
                );
                State.ui.highlightedParams = newlyAddedParams;
                Core.saveConfig();
                UI.renderMainContent();
              } else {
                UI.showToast(Utils.t("toasts.paramsExist"));
              }
            },
            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 = Utils.findLinkInEvent(e);
              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 = Utils.findLinkInEvent(e);
              if (
                link &&
                typeof link.dataset[State.invalidAttrName] === "undefined"
              ) {
                const finalRules = Core.getRulesForUrl(link.href);
                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");
                }
                if (!finalRules.compatibilityMode) {
                  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();
    const defaultLocale = I18nManager.getDefaultLocale();

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

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

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

  main();
})();