Greasy Fork

Greasy Fork is available in English.

全局解除选中、复制与右键限制

精细控制网页限制解除行为 + 可配置强力解除全部限制模式 + 全部关闭模式 (Canvas增强) + 支持用户本地配置规则(本地规则优先于远程)(完结撒花)

当前为 2025-05-10 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          全局解除选中、复制与右键限制
// @namespace     http://tampermonkey.net/
// @version       1.4.2
// @description   精细控制网页限制解除行为 + 可配置强力解除全部限制模式 + 全部关闭模式 (Canvas增强) + 支持用户本地配置规则(本地规则优先于远程)(完结撒花)
// @author        yui酱
// @match         *://*/*
// @grant         GM_xmlhttpRequest
// @grant         GM_setValue
// @grant         GM_getValue
// @connect       raw.githubusercontent.com
// @connect       cdn.jsdelivr.net // 新增:允许连接 jsDelivr CDN
// @license       MIT
// ==/UserScript==

(function () {
  'use strict';
  // 注意: 下面两行会在每次脚本运行时清除配置缓存,主要用于开发调试阶段。您想打开来玩一玩也行(
  //GM_setValue('remote_config_cache', null);
  //GM_setValue('remote_config_cache_time', 0);

  // --- 用户本地配置区域 ---
  //
  // 如果您希望自定义规则,并让这些规则优先于远程配置,请在这里定义您的 `localRules` 数组。
  // 定义并启用 (即移除下面的 `/* ... */` 注释) 后,脚本将尝试下载远程配置,
  // 然后将本地规则合并进去。如果本地和远程有相同的 `domain` 规则,本地的将覆盖远程的。
  //
  // 注意:以下示例规则是为演示用途,您可以根据您的需求修改或添加规则。
  // 请确保规则的格式符合 JSON 标准。
  //
  // 示例 (移除以下 `/*` 和 `*/` 符号来启用):
/*
  const localRules = [
    {
      "domain": "\\.example\\.com$", // 匹配 example.com 及其子域名
      "forceUnlockAll": true,         // 在这个网站强制解除所有限制
      "unlockOverlayPro": false,
      "unlockOff": false
    },
    {
      "domain": "\\.another-site\\.org$", // 匹配 another-site.org 及其子域名
      "unlockSelect": false,            // 在这个网站禁用选中
      "unlockCopy": true,
      "unlockContext": false,
      "unlockOverlay": false,
      "unlockOverlayPro": false,
      "unlockOff": false
    },
    {
      "domain": "\\.offline-tool\\.io$", // 匹配 offline-tool.io 及其子域名,完全禁用解除限制
      "unlockOff": true                 // 在这个网站禁用所有解除限制功能
    },
    // 您可以在这里添加更多规则...
    // 例如:
    // {
    //   "domain": "\\.some-forum\\.net$",
    //   "unlockCopy": true,
    //   "unlockContext": true
    // }
  ];
  */

  // 请将上面的注释符号 (/* ... */) 移除,并在其中填入您自己的规则。
  // 如果不使用本地配置,请保持这段代码注释或将 `localRules` 变量移除。
  // --- 用户本地配置区域结束 ---




  // Debounce utility function - 用于限制函数调用频率
  function debounce(func, delay) {
    let timeout;
    return function(...args) {
      const context = this;
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(context, args), delay);
    };
  }

  // 远程配置文件的URL
  // 主链接使用 GitHub Raw
  const CONFIG_URL = 'https://raw.githubusercontent.com/MT-Y-TM/Fuck_All_Web_Restrictions/main/config.json';
  // 备用链接使用 jsDelivr CDN 指向您的 GitHub 仓库
    const CONFIG_URL_CN = 'https://cdn.jsdelivr.net/gh/MT-Y-TM/Fuck_All_Web_Restrictions@main/config.json';
  // 本地缓存配置的键名
  const CACHE_KEY = 'remote_config_cache';
  // 本地缓存配置时间的键名
  const CACHE_TIME_KEY = 'remote_config_cache_time';
  // 缓存有效期(7天,单位:毫秒)
  const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000;

  // 获取远程配置的内部辅助函数,处理 URL 切换和回调
  // remoteCallback 现在接收 { config: parsedJson, source: 'github' | 'jsdelivr' | 'error' | 'cache' }
  function _fetchRemoteConfigInternal(primaryUrl, fallbackUrl, remoteCallback, cachedConfigText, useCnUrl) {
    const urlToFetch = useCnUrl ? fallbackUrl : primaryUrl;
    const urlName = useCnUrl ? 'jsDelivr (备用)' : 'GitHub Raw (主)'; // 更新日志名称

    console.log(`[解除限制] 尝试从 ${urlName} 获取配置文件: ${urlToFetch}`);

    GM_xmlhttpRequest({
      method: 'GET',
      url: urlToFetch,
      onload: function (response) {
        if (response.status === 200) {
          try {
            const json = JSON.parse(response.responseText);
            GM_setValue(CACHE_KEY, response.responseText); // 缓存新的配置文本
            GM_setValue(CACHE_TIME_KEY, Date.now());     // 更新缓存时间
            // console.log(`[解除限制] 配置文件从 ${urlName} 获取成功。`); // 移除这里的成功日志,由 loadConfig 统一输出
            const source = useCnUrl ? 'jsdelivr' : 'github';
            remoteCallback({ config: json, source: source }); // 成功:传递解析后的配置和来源
          } catch (e) {
            console.error(`[解除限制] 从 ${urlName} 获取的配置文件解析失败:`, e);
            if (!useCnUrl) {
              // 如果主链接失败,尝试备用链接
              _fetchRemoteConfigInternal(primaryUrl, fallbackUrl, remoteCallback, cachedConfigText, true);
            } else {
              console.error('[解除限制] 备用配置文件也解析失败。');
              // 两个 URL 都失败,尝试使用现有的缓存配置(如果可用),并标记来源为 error
              remoteCallback({ config: cachedConfigText ? JSON.parse(cachedConfigText) : null, source: 'error' });
            }
          }
        } else {
          console.error(`[解除限制] 从 ${urlName} 获取配置文件失败,状态码: ${response.status}`);
          if (!useCnUrl) {
            // 如果主链接失败,尝试备用链接
            _fetchRemoteConfigInternal(primaryUrl, fallbackUrl, remoteCallback, cachedConfigText, true);
          } else {
            console.error('[解除限制] 备用配置文件也获取失败。');
            // 两个 URL 都失败,尝试使用现有的缓存配置(如果可用),并标记来源为 error
            remoteCallback({ config: cachedConfigText ? JSON.parse(cachedConfigText) : null, source: 'error' });
          }
        }
      },
      onerror: function (error) {
        console.error(`[解除限制] 从 ${urlName} 获取配置文件网络错误:`, error);
        if (!useCnUrl) {
          // 如果主链接网络错误,尝试备用链接
          _fetchRemoteConfigInternal(primaryUrl, fallbackUrl, remoteCallback, cachedConfigText, true);
        } else {
          console.error('[解除限制] 备用配置文件也网络错误。');
          // 两个 URL 都失败,尝试使用现有的缓存配置(如果可用),并标记来源为 error
          remoteCallback({ config: cachedConfigText ? JSON.parse(cachedConfigText) : null, source: 'error' });
        }
      }
    });
  }

  // 加载配置函数:负责获取远程配置并与本地配置合并
  // callback 现在接收 config 对象
  function loadConfig(callback) {
    // 先尝试获取远程配置(或使用缓存)
    fetchRemoteConfig(function(result) { // result 是 { config: ..., source: ... }
      const { config: remoteConfig, source: remoteSource } = result; // 解构结果

      let finalConfig = remoteConfig || { rules: [] }; // 初始化最终配置,确保有 rules 数组
      let configSource = remoteSource; // 记录配置的最终来源

      // 检查 `localRules` 变量是否已定义、是数组且不为空
      // 注意:`localRules` 是用户可以在脚本顶部的“用户本地配置区域”定义的全局常量。
      if (typeof localRules !== 'undefined' && Array.isArray(localRules) && localRules.length > 0) {
        console.log('%c[解除限制] 检测到用户本地配置规则,将与远程配置合并。本地规则(domain 相同者)将覆盖远程配置。', 'color: #8A2BE2; font-weight: bold;');

        // 创建一个临时 Map 用于高效合并规则,以 `domain` 字符串作为键
        const mergedRulesMap = new Map();

        // 首先将远程规则添加到 Map 中
        if (finalConfig.rules) { // Ensure remote rules exist
          finalConfig.rules.forEach(rule => {
            // 仅将具有 'domain' 属性的有效远程规则添加到 Map 中,忽略注释对象
            if (rule && typeof rule.domain === 'string') {
               mergedRulesMap.set(rule.domain, rule);
            }
          });
        }


        // 然后遍历本地规则,添加到 Map 中。如果 `domain` 已存在,本地规则将覆盖远程规则
        localRules.forEach(localRule => {
           // 仅处理具有 'domain' 属性的有效本地规则
           if (localRule && typeof localRule.domain === 'string') {
              mergedRulesMap.set(localRule.domain, localRule);
           }
        });

        // 将合并后的 Map 转换回数组,作为最终的规则列表
        finalConfig.rules = Array.from(mergedRulesMap.values());

        // 如果使用了本地规则合并,来源就包含本地信息
        configSource = (remoteSource && remoteSource !== 'error') ? `远程 (${remoteSource}) + 本地` : '仅本地';

      } else {
        console.log('[解除限制] 未检测到用户本地配置规则。');
        // 如果没有本地规则,也需要过滤掉远程配置中的注释对象
        if (finalConfig.rules) { // 确保 rules 存在
           finalConfig.rules = finalConfig.rules.filter(rule => rule && typeof rule.domain === 'string');
        }
        // configSource 已经是远程来源或 error
      }

      // *** 在这里输出配置加载完成的日志,明确来源 ***
      const sourceMessage = configSource === 'cache' ? '本地缓存' :
                            configSource === 'github' ? '远程仓库 (主链接)' :
                            configSource === 'jsdelivr' ? '远程仓库 (备用链接)' :
                            configSource === 'error' ? '远程加载失败,使用本地缓存或空配置' : // 根据 error source 调整
                            configSource; // 处理合并来源或其他情况

      console.log(`%c[解除限制] 配置加载完成。来源: ${sourceMessage}`, 'color: #1A73E8; font-weight: bold;');


      // 将最终合并的配置传递给主脚本逻辑
      callback(finalConfig);
    });
  }

  // 获取远程配置的入口函数
  // remoteCallback 现在接收 { config: parsedJson, source: 'cache' | 'github' | 'jsdelivr' | 'error' }
  function fetchRemoteConfig(remoteCallback) {
    const now = Date.now();
    const cachedTime = GM_getValue(CACHE_TIME_KEY, 0);
    const cachedConfigText = GM_getValue(CACHE_KEY, null); // 获取缓存的文本

    // 如果存在缓存配置且未过期
    if (cachedConfigText && (now - cachedTime < CACHE_DURATION)) {
      try {
        const parsedConfig = JSON.parse(cachedConfigText);
        //console.log('[解除限制] 配置文件从本地缓存加载成功。'); // 移除这里的成功日志,由 loadConfig 统一输出
        remoteCallback({ config: parsedConfig, source: 'cache' }); // 传递解析后的缓存配置和来源
      } catch (e) {
        console.error('解析缓存配置失败:', e);
        // 如果缓存解析失败,则尝试从远程获取(从主 URL 开始)
        _fetchRemoteConfigInternal(CONFIG_URL, CONFIG_URL_CN, remoteCallback, cachedConfigText, false);
      }
    } else {
      // 否则,从远程获取配置(从主 URL 开始)
      _fetchRemoteConfigInternal(CONFIG_URL, CONFIG_URL_CN, remoteCallback, cachedConfigText, false);
    }
  }

  //控制台输出确认脚本工作状态
  function logCurrentRules(rule, hostname) {
    const COLOR_MAIN_TITLE = 'color: #1A73E8; font-weight: bold;'; // 主标题蓝色
    const COLOR_SEPARATOR = 'color: #4CAF50;';                   // 分隔符绿色
    const COLOR_LABEL_HIGHLIGHT = 'color: #FF5722; font-weight: bold;'; // 标签文字的颜色(橙色)
    const COLOR_TRUE = 'color: #28A745; font-weight: bold;';      // true 的绿色
    const COLOR_FALSE = 'color: #DC3545; font-weight: bold;';     // false 的红色

    function getBoolColor(value) {
      return value ? COLOR_TRUE : COLOR_FALSE;
    }

    console.log(
      '%c[脚本的使用规则情况]%c: %c强力解除%c:%c' + rule.forceUnlockAll +
      '%c;%c右键%c:%c' + rule.unlockContext +
      '%c;%c遮罩%c:%c' + rule.unlockOverlay +
      '%c;%c强力遮罩%c:%c' + rule.unlockOverlayPro +
      '%c;%c选中%c:%c' + rule.unlockSelect +
      '%c;%c复制%c:%c' + rule.unlockCopy +
      '%c;%c全部禁用%c:%c' + rule.unlockOff,

      COLOR_MAIN_TITLE,
      COLOR_SEPARATOR,
      COLOR_LABEL_HIGHLIGHT,
      COLOR_SEPARATOR,
      getBoolColor(rule.forceUnlockAll),

      COLOR_SEPARATOR,
      COLOR_LABEL_HIGHLIGHT,
      COLOR_SEPARATOR,
      getBoolColor(rule.unlockContext),

      COLOR_SEPARATOR,
      COLOR_LABEL_HIGHLIGHT,
      COLOR_SEPARATOR,
      getBoolColor(rule.unlockOverlay),

      COLOR_SEPARATOR,
      COLOR_LABEL_HIGHLIGHT,
      COLOR_SEPARATOR,
      getBoolColor(rule.unlockOverlayPro),

      COLOR_SEPARATOR,
      COLOR_LABEL_HIGHLIGHT,
      COLOR_SEPARATOR,
      getBoolColor(rule.unlockSelect),

      COLOR_SEPARATOR,
      COLOR_LABEL_HIGHLIGHT,
      COLOR_SEPARATOR,
      getBoolColor(rule.unlockCopy),

      COLOR_SEPARATOR,
      COLOR_LABEL_HIGHLIGHT,
      COLOR_SEPARATOR,
      getBoolColor(rule.unlockOff)
    );
  }


  // 加载配置并执行主要逻辑
  loadConfig(function (config) {
    // 默认规则
    const defaultRule = {
      unlockSelect: true,     // 是否解除选中限制
      unlockCopy: true,       // 是否解除复制/剪切限制
      unlockContext: true,    // 是否解除右键菜单限制
      unlockOverlay: true,    // 是否移除遮罩层 (常规版)
      unlockOverlayPro: false, // 是否移除遮罩层 (强力版)
      forceUnlockAll: false,   // 是否启用强力解除所有限制模式
      unlockOff: false         // 新增:是否禁用所有解除限制功能 (默认不禁用)
    };

    const hostname = location.hostname; // 当前页面的域名
    const rules = config?.rules || []; // 从合并后的配置中获取规则列表,若无则为空数组

    // 查找适用于当前站点的规则,忽略不包含 'domain' 属性的对象(即注释对象)
    const siteRule = rules.find(r => {
      // 确保 r 存在且有 domain 属性
      if (r && typeof r.domain === 'string') {
         try {
            return new RegExp(r.domain, 'i').test(hostname);
         } catch (e) {
            console.warn(`[解除限制] 无效的域名正则表达式 '${r.domain}':`, e);
            return false; // 如果正则表达式无效,则忽略此规则
         }
      }
      return false; // 忽略没有 domain 属性的对象
    });


    const rule = { ...defaultRule, ...(siteRule || {}) }; // 合并规则,站点规则优先

    const COLOR_PINK_MESSAGE = 'color: #C71585; font-weight: bold;';
    console.log(
      '%c[解除限制] 脚本已加载并应用规则 for: %c' + hostname,
      COLOR_PINK_MESSAGE,
      COLOR_PINK_MESSAGE
    );

    // --- 最高优先级:如果 unlockOff 为 true,则禁用所有解除限制功能并退出 ---
    if (rule.unlockOff) {
        // 输出用户指定的日志信息
        console.log('%c[解除限制] 本脚本已检测到该网站全局禁用模式 (unlockOff) 已启用,已关闭本脚本的运行', 'color: #FF4500; font-weight: bold;');

        // Explicitly set all other unlock flags to false as requested
        rule.unlockSelect = false;
        rule.unlockCopy = false;
        rule.unlockContext = false;
        rule.unlockOverlay = false;
        rule.unlockOverlayPro = false;
        rule.forceUnlockAll = false;
        // rule.unlockOff remains true

        // 打印最终的规则状态(所有解除功能应为 false)
        logCurrentRules(rule, hostname);
        // 使用 return 语句停止脚本的后续执行,释放资源
        return;
    }
    // --- 最高优先级逻辑结束 ---


    // --- 处理 unlockOverlay 与 unlockOverlayPro 的互斥逻辑 ---
    // 这一逻辑在 unlockOff 之后执行,因为它调整的是具体解除功能的参数
    if (rule.unlockOverlay && rule.unlockOverlayPro) {
      console.warn('%c[警告] 站点规则同时启用了“遮罩”和“强力遮罩”。“强力遮罩”将优先,常规“遮罩”将被禁用。', 'color: orange; font-weight: bold;');
      rule.unlockOverlay = false; // 强力遮罩优先,禁用常规遮罩
    }
    // --- 互斥逻辑结束 ---

    // 是否应该启用强力模式
    const shouldForce = rule.forceUnlockAll;

    // 🔒 强力解除模式 (forceUnlockAll)
    // 如果强力模式被启用,则执行强力解除,并退出脚本
    if (shouldForce) {
      [
        'copy', 'cut', 'selectstart', 'mousedown', 'mouseup',
        'mousemove', 'contextmenu', 'keydown', 'keypress',
        'keyup', 'selectionchange', 'dragstart', 'drag', 'dragend'
      ].forEach(evt => {
        document.addEventListener(evt, e => {
          e.stopImmediatePropagation();
        }, true);
        try {
          Object.defineProperty(document, `on${evt}`, { value: null, writable: true, configurable: true });
          Object.defineProperty(window, `on${evt}`, { value: null, writable: true, configurable: true });
          Object.defineProperty(document.body, `on${evt}`, { value: null, writable: true, configurable: true });
        } catch (e) { /* 对于无法重新定义的属性,静默处理错误 */ }
      });

      const forceStyle = document.createElement('style');
      forceStyle.textContent = `
        html, body, *, ::before, ::after {
          -webkit-user-select: text !important;
          -moz-user-select: text !important;
          -ms-user-select: text !important;
          user-select: text !important;
          -webkit-touch-callout: default !important;
          pointer-events: auto !important;
          cursor: auto !important; /* 强力模式下包含光标重置 */
        }
        canvas {
            pointer-events: auto !important;
            -webkit-user-select: auto !important;
            user-select: auto !important;
        }
      `;
      (document.head || document.documentElement).appendChild(forceStyle);

      // 清除所有定时器,这个操作要非常谨慎,可能影响页面正常功能
      let M_interval = setInterval(()=>{},99999); for(let i=0;i<M_interval;i++)clearInterval(i);
      let M_timeout = setTimeout(()=>{},99999); for(let i=0;i<M_timeout;i++)clearTimeout(i);

      console.log('[解除限制] 强力模式已启用 (Canvas Enhanced).');
      logCurrentRules(rule, hostname); // 打印最终应用规则状态
      return; // 强力模式启用后,不执行常规解除逻辑
    }

    // ✅ 常规解除选中限制
    if (rule.unlockSelect) {
      const selectStyle = document.createElement('style');
      selectStyle.id = 'unlock-select-style'; // 给style标签一个ID,方便调试
      selectStyle.textContent = `
        *, ::before, ::after { /* 应用于所有元素和伪元素 */
          -webkit-user-select: auto !important;
          -moz-user-select: auto !important;
          -ms-user-select: auto !important;
          user-select: auto !important;    /* 允许自动选择 */
        }
      `;
      (document.head || document.documentElement).appendChild(selectStyle);

      // 主要针对选择相关的事件
      ['selectstart', 'mousedown', 'dragstart'].forEach(evt => {
        document.addEventListener(evt, e => e.stopPropagation(), true);
        try {
          Object.defineProperty(document, `on${evt}`, { value: null, writable: true, configurable: true });
          Object.defineProperty(document.body, `on${evt}`, { value: null, writable: true, configurable: true });
        } catch (e) { /* 静默处理错误 */ }
      });
      // console.log('[解除限制] 解除选中限制已启用。'); // 可以按需开启详细日志
    }

    // ✅ 常规解除复制/剪切限制
    if (rule.unlockCopy) {
      // 主要针对复制、剪切相关的事件
      ['copy', 'cut'].forEach(evt => {
        document.addEventListener(evt, e => e.stopPropagation(), true);
        try {
          Object.defineProperty(document, `on${evt}`, { value: null, writable: true, configurable: true });
          Object.defineProperty(document.body, `on${evt}`, { value: null, writable: true, configurable: true });
        } catch (e) { /* 静默处理错误 */ }
      });
      // console.log('[解除限制] 解除复制/剪切限制已启用。'); // 可以按需开启详细日志
    }

    // ✅ 恢复右键菜单
    if (rule.unlockContext) {
      document.addEventListener('contextmenu', e => {
        e.stopImmediatePropagation();
      }, true);

      try {
        Object.defineProperty(document, 'oncontextmenu', { value: null, writable: true, configurable: true });
        Object.defineProperty(document.body, 'oncontextmenu', { value: null, writable: true, configurable: true });
        Object.defineProperty(document.documentElement, 'oncontextmenu', { value: null, writable: true, configurable: true });
      } catch (e) { /* 静默处理错误 */ }

      // 动态处理,确保新添加的元素也移除右键菜单限制
      // 原有的 MutationObserver 逻辑已优化
    }

    // ✅ 移除遮罩层 (常规版)
    if (rule.unlockOverlay) {
      // 将 removeOverlays 包裹在 debounce 中
      const debouncedRemoveOverlays = debounce(function removeOverlays() {
        // 找... // 查找 body 下的直接子元素,通常遮罩层不会在很深的嵌套中
        document.querySelectorAll('body > *').forEach(el => {
          const st = window.getComputedStyle(el);
          if (!st) return;
          // 常见遮罩层的判断条件:固定或绝对定位,高 z-index,透明或无背景色,且阻止鼠标事件
          if ((st.position === 'fixed' || st.position === 'absolute') &&
              st.zIndex !== 'auto' && parseInt(st.zIndex) > 999 && // 假定高 z-index
              st.pointerEvents === 'none' &&
              (st.backgroundColor === 'rgba(0, 0, 0, 0)' || parseFloat(st.opacity) < 0.1)) {
             // console.log('[解除限制] 移除疑似遮罩层 (常规):', el); // 可选日志
             el.remove();
          }
        });
        // 针对常见的阻止滚动的样式,尝试移除
        if (document.body.style.overflow === 'hidden') {
            document.body.style.removeProperty('overflow');
        }
        if (document.documentElement.style.overflow === 'hidden') {
            document.documentElement.style.removeProperty('overflow');
        }
      }, 100); // 100ms 防抖

      // 在页面加载后和滚动时尝试移除
      window.addEventListener('load', debouncedRemoveOverlays, true);
      window.addEventListener('scroll', debouncedRemoveOverlays, true);
      // 也可以添加一个 MutationObserver 观察 body 的子元素变化
      const overlayObserver = new MutationObserver(debouncedRemoveOverlays);
      overlayObserver.observe(document.body, { childList: true, subtree: false }); // 只观察直接子元素
    }

    // ✅ 移除遮罩层 (强力版) - unlockOverlayPro
    if (rule.unlockOverlayPro) {
        const overlayProStyle = document.createElement('style');
        overlayProStyle.id = 'unlock-overlay-pro-style'; // 方便调试
        overlayProStyle.textContent = `
            html, body, *, ::before, ::after {
                pointer-events: auto !important; /* 恢复所有元素的鼠标事件 */
                cursor: auto !important;        /* 重置光标 */
            }
            /* 尝试隐藏常见的模态框或遮罩层类名 */
            .modal-backdrop, .overlay, .dark-screen, .popup-wrapper, .dialog-container {
                display: none !important;
            }
            /* 针对一些高度/宽度都为100%的fixed/absolute元素进行隐藏 */
            div[style*="position:fixed"][style*="height:100%"][style*="width:100%"],
            div[style*="position:absolute"][style*="height:100%"][style*="width:100%"] {
                display: none !important;
            }
            body {
                overflow: auto !important; /* 确保 body 滚动条可用 */
            }
        `;
        (document.head || document.documentElement).appendChild(overlayProStyle);

        // 将 removeOverlayProElementsAggressively 包裹在 debounce中
        const debouncedRemoveOverlayProElementsAggressively = debounce(function removeOverlayProElementsAggressively() {
            document.querySelectorAll('body > *').forEach(el => {
                const st = window.getComputedStyle(el);
                if (!st) return;

                const isFixedOrAbsolute = (st.position === 'fixed' || st.position === 'absolute');
                const hasHighZIndex = st.zIndex !== 'auto' && parseInt(st.zIndex) > 10000; // 极高 z-index
                const isFullScreenLike = (st.width === '100vw' || st.width === '100%') && (st.height === '100vh' || st.height === '100%');
                const isClickBlocking = st.pointerEvents === 'none' || st.cursor === 'not-allowed';

                // 判断条件可以更复杂,例如结合背景色透明度、尺寸等
                if (isFixedOrAbsolute && hasHighZIndex && (isFullScreenLike || isClickBlocking)) {
                    // console.log('[解除限制] 强力移除疑似遮罩层 (Pro):', el); // 可选日志
                    el.remove();
                }
            });
            // 移除可能被设置的 body/html 上的阻止滚动的行内样式或类
            document.body.style.removeProperty('overflow');
            document.body.classList.remove('no-scroll', 'modal-open');
            document.documentElement.style.removeProperty('overflow');
            document.documentElement.classList.remove('no-scroll', 'modal-open');
        }, 200); // 200ms 防抖,因为这个函数更激进

        // 立即执行,并持续观察 DOM 变化
        debouncedRemoveOverlayProElementsAggressively();
        window.addEventListener('load', debouncedRemoveOverlayProElementsAggressively, true);
        // MutationObserver 观察 body 下所有子元素的增删,以应对动态生成的遮罩
        const overlayProObserver = new MutationObserver(debouncedRemoveOverlayProElementsAggressively);
        overlayProObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
    }


    // 观察 DOM 变化,以便在必要时重新应用右键菜单规则
    // 对 MutationObserver 的回调进行防抖和优化
    const debouncedUnlockContextObserverCallback = debounce((mutationsList) => {
        if (!rule.unlockContext) return; // 确保规则仍然激活

        mutationsList.forEach(mutation => {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) { // 只处理元素节点
                        // 检查新增节点本身
                        if (node.oncontextmenu) node.oncontextmenu = null;
                        if (node.hasAttribute('oncontextmenu')) node.removeAttribute('oncontextmenu');
                        // 检查新增节点的子孙元素
                        node.querySelectorAll('*').forEach(el => {
                            if (el.oncontextmenu) el.oncontextmenu = null;
                            if (el.hasAttribute('oncontextmenu')) el.removeAttribute('oncontextmenu');
                        });
                    }
                });
            } else if (mutation.type === 'attributes' && mutation.attributeName === 'oncontextmenu') {
                // 只处理 oncontextmenu 属性变化的元素本身
                const targetElement = mutation.target;
                if (targetElement.oncontextmenu) targetElement.oncontextmenu = null;
                if (targetElement.hasAttribute('oncontextmenu')) targetElement.removeAttribute('oncontextmenu');
            }
        });
    }, 150); // 150ms 防抖

    const observer = new MutationObserver(debouncedUnlockContextObserverCallback);
    observer.observe(document.documentElement, { attributes: true, childList: true, subtree: true });

    logCurrentRules(rule, hostname); // 打印最终应用规则状态
  }); // loadConfig 回调结束
})();