Watermark Remover Suite

通用低风险去水印、站点专用处理(含 ZSXQ & 自动学习站点规则);高风险按钮支持两级策略:第一次点击使用“智能识别水印图片+全屏遮罩”有限清理并自动学习规则,若仍未去除,第二次点击则启用全量暴力清理。支持配置持久化与高优先级 CSS 直杀水印。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Watermark Remover Suite
// @namespace    https://blog.wayneshao.com/
// @version      1.7
// @description  通用低风险去水印、站点专用处理(含 ZSXQ & 自动学习站点规则);高风险按钮支持两级策略:第一次点击使用“智能识别水印图片+全屏遮罩”有限清理并自动学习规则,若仍未去除,第二次点击则启用全量暴力清理。支持配置持久化与高优先级 CSS 直杀水印。
// @author       You
// @match        *://*/*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const LOG_PREFIX = '[Watermark Remover]';

  /* ========== 全局配置 ========== */
  const BUTTON_ID = 'wm-remover-sweep-btn';
  const BASE_STYLE_ID = 'wm-remover-base-style';
  const ZSXQ_STYLE_ID = 'wm-remover-zsxq-style';
  const GLOBAL_AGGRESSIVE_STYLE_ID = 'wm-remover-aggressive-style';
  const HARD_KILL_STYLE_ID = 'wm-remover-hard-kill-style';
  const SITE_RULES_STYLE_ID = 'wm-remover-site-rules-style';
  const BUTTON_STORAGE_KEY = 'wm-remover-button-pos-v1';
  const SITE_RULES_KEY = 'wm-remover-site-rules-v1';

  // 图像水印识别参数
  const MAX_SAMPLE_SIZE = 512;       // 图像降采样最大边
  const WATERMARK_SCORE_THRESHOLD = 0.6; // >= 这个分数认为是水印图

  const isZsxqDomain = /(^|\.)zsxq\.com$/i.test(window.location.hostname);

  /* ========== 站点规则存储结构 ========== */
  // allConfigs: { [hostname]: { domain: string, rules: Array<{selector:string, props:string[], note?:string}> } }

  function loadAllSiteConfigs() {
    try {
      if (typeof GM_getValue === 'function') {
        return GM_getValue(SITE_RULES_KEY) || {};
      } else if (window.localStorage) {
        const raw = localStorage.getItem(SITE_RULES_KEY);
        return raw ? JSON.parse(raw) : {};
      }
    } catch (e) {
      console.debug(`${LOG_PREFIX} 读取站点规则失败:`, e);
    }
    return {};
  }

  function saveAllSiteConfigs(all) {
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue(SITE_RULES_KEY, all);
      } else if (window.localStorage) {
        localStorage.setItem(SITE_RULES_KEY, JSON.stringify(all));
      }
    } catch (e) {
      console.debug(`${LOG_PREFIX} 保存站点规则失败:`, e);
    }
  }

  function loadSiteConfig(host) {
    const all = loadAllSiteConfigs();
    if (!all[host]) all[host] = { domain: host, rules: [] };
    return all[host];
  }

  function saveSiteConfig(host, cfg) {
    const all = loadAllSiteConfigs();
    all[host] = cfg;
    saveAllSiteConfigs(all);
  }

  function cssEscape(s) {
    return String(s).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, '\\$1');
  }

  function cssFromRule(rule) {
    const sel = rule.selector;
    const selWithStyle = `${sel}[style]`;
    const lines = (rule.props || []).map(
      (p) => `  ${p}: none !important;`
    ).join('\n');

    return `
${selWithStyle},
${sel} {
${lines}
}
${sel}::before,
${sel}::after {
  background: none !important;
  background-image: none !important;
  mask-image: none !important;
  -webkit-mask-image: none !important;
}
`;
  }

  function injectCssForSiteRules(host) {
    const cfg = loadSiteConfig(host);
    if (!cfg.rules || !cfg.rules.length) return;

    let style = document.getElementById(SITE_RULES_STYLE_ID);
    if (!style) {
      style = document.createElement('style');
      style.id = SITE_RULES_STYLE_ID;
      (document.head || document.documentElement).appendChild(style);
    }

    let cssText = '';
    cfg.rules.forEach((rule) => {
      cssText += cssFromRule(rule);
    });
    style.textContent = cssText;

    console.info(`${LOG_PREFIX} 已注入站点专用规则`, host, cfg.rules);
  }

  /* ========== 全局状态 ========== */
  let sweepButton = null;
  let suppressClick = false;

  let isHighRiskSweeping = false;
  let aggressiveTimer = null;

  const dragState = {
    active: false,
    moved: false,
    pointerId: null,
    startX: 0,
    startY: 0,
  };

  let buttonPos = loadButtonPosition();

  const lowRiskObserver = new MutationObserver(handleLowRiskMutations);

  const specialHandlers = [
    {
      name: 'zsxq',
      test: () => isZsxqDomain,
      init: setupZsxqHandler,
    },
  ];

  // 高风险遍历中使用的状态
  const processedCountMap = new WeakMap();
  const HARD_KILL_THRESHOLD = 5;
  let overlayCandidates = new Set();

  // 本次页面中,用户点击高风险按钮的次数(1 = 智能模式,>=2 = 纯暴力)
  let highRiskClickCount = 0;

  /* ========== document-start 初始化:先注入站点规则 CSS ========== */

  injectCssForSiteRules(location.hostname);

  /* ========== ready 后初始化 ========== */
  whenReady(() => {
    injectBaseCss();
    injectHardKillCss();
    injectGlobalAggressiveCss();
    setupRestoreDetection();
    ensureSweepButton();
    startLowRiskLogic();
    runSpecialHandlers();
  });

  /* ========== 通用低风险逻辑 ========== */
  function startLowRiskLogic() {
    lowRiskSweep(document);

    const startObserver = () => {
      if (!document.body) return false;
      lowRiskObserver.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style', 'class', 'watermark', 'data-watermark', 'data-testid'],
      });
      return true;
    };

    if (!startObserver()) {
      const watcher = new MutationObserver(() => {
        if (startObserver()) watcher.disconnect();
      });
      watcher.observe(document.documentElement, { childList: true });
    }
  }

  function handleLowRiskMutations(mutations) {
    if (isHighRiskSweeping) return;

    for (const mutation of mutations) {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach((node) => lowRiskSweep(node));
      } else if (mutation.type === 'attributes') {
        lowRiskProcessElement(mutation.target);
      }
    }
  }

  function lowRiskSweep(root) {
    if (!root) return;
    const startNode = root instanceof Document ? root.documentElement : root;
    walkDom(startNode, lowRiskProcessElement);
  }

  function isWatermarkLike(el) {
    if (!(el instanceof Element)) return false;
    if (
      el.hasAttribute('watermark') ||
      el.hasAttribute('data-watermark') ||
      (el.classList && [...el.classList].some((cls) => /watermark/i.test(cls)))
    ) {
      return true;
    }
    const testId = el.getAttribute('data-testid');
    if (testId && /watermark/i.test(testId)) return true;
    return false;
  }

  function lowRiskProcessElement(el) {
    if (!(el instanceof Element) || shouldSkipButton(el)) return;

    const inlineStyle = el.getAttribute('style');
    if (inlineStyle && /nullbackground/i.test(inlineStyle)) {
      el.setAttribute('style', inlineStyle.replace(/nullbackground/gi, 'background'));
    }

    const backgroundImage = el.style.getPropertyValue('background-image');
    if (backgroundImage && /url\(\s*data:image/i.test(backgroundImage)) {
      el.style.setProperty('background-image', 'none', 'important');
    }

    const background = el.style.getPropertyValue('background');
    if (background && /url\(\s*data:image/i.test(background)) {
      el.style.setProperty(
        'background',
        background.replace(/url\([^)]*\)/gi, 'none').trim(),
        'important'
      );
    }

    const maskImage =
      el.style.getPropertyValue('mask-image') ||
      el.style.getPropertyValue('-webkit-mask-image');
    if (maskImage && /url\(\s*data:image/i.test(maskImage)) {
      el.style.setProperty('mask-image', 'none', 'important');
      el.style.setProperty('-webkit-mask-image', 'none', 'important');
    }

    if (isWatermarkLike(el)) {
      el.style.setProperty('background', 'none', 'important');
      el.style.setProperty('background-image', 'none', 'important');
      el.style.setProperty('mask-image', 'none', 'important');
      el.style.setProperty('-webkit-mask-image', 'none', 'important');
    }
  }

  /* ========== 站点专用逻辑(目前仅 ZSXQ) ========== */
  function runSpecialHandlers() {
    for (const handler of specialHandlers) {
      try {
        if (handler.test()) {
          handler.init();
        }
      } catch (err) {
        console.warn(`${LOG_PREFIX} 站点专用逻辑 ${handler.name} 初始化失败`, err);
      }
    }
  }

  function setupZsxqHandler() {
    injectZsxqCss();

    const state = {
      observer: null,
      observedRoots: new WeakSet(),
      interactionTimer: null,
    };

    const ensureObserver = () => {
      if (state.observer) return;
      state.observer = new MutationObserver((mutations) => {
        if (isHighRiskSweeping) return;

        for (const mutation of mutations) {
          if (mutation.type === 'childList') {
            mutation.addedNodes.forEach((node) => processNode(node));
          } else if (mutation.type === 'attributes') {
            const target = mutation.target;
            if (target instanceof Element && shouldClearZsxqElement(target)) {
              requestAnimationFrame(() => clearZsxqWatermark(target));
            }
          }
        }
      });
      state.observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style', 'watermark', 'data-watermark', 'class', 'data-testid'],
      });
    };

    const attachBodyObserver = () => {
      if (!document.body || state.observedRoots.has(document.body)) return false;
      state.observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style'],
      });
      state.observedRoots.add(document.body);
      return true;
    };

    const processNode = (node) => {
      if (node instanceof Element) {
        if (node.shadowRoot) {
          observeShadowRoot(node.shadowRoot);
          scanZsxqElements(node.shadowRoot);
        }
        requestAnimationFrame(() => scanZsxqElements(node));
      } else if (node instanceof ShadowRoot || node instanceof DocumentFragment) {
        observeShadowRoot(node);
        requestAnimationFrame(() => scanZsxqElements(node));
      }
    };

    const observeShadowRoot = (root) => {
      if (!state.observer || state.observedRoots.has(root)) return;
      try {
        state.observer.observe(root, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeFilter: ['style', 'class', 'watermark', 'data-watermark', 'data-testid'],
        });
        state.observedRoots.add(root);
      } catch (err) {
        console.debug(`${LOG_PREFIX} 无法监听 ShadowRoot:`, err);
      }
    };

    const scanZsxqElements = (root) => {
      const startNode = root instanceof Document ? root.documentElement : root;
      if (!startNode) return;
      walkDom(startNode, (el) => {
        if (shouldClearZsxqElement(el)) {
          clearZsxqWatermark(el);
        }
      });
    };

    const scheduleRescanAfterInteraction = () => {
      if (state.interactionTimer) clearTimeout(state.interactionTimer);
      state.interactionTimer = setTimeout(() => {
        state.interactionTimer = null;
        if (document.body) scanZsxqElements(document.body);
      }, 250);
    };

    const shouldClearZsxqElement = (el) => {
      if (!(el instanceof Element) || shouldSkipButton(el)) return false;

      if (
        el.hasAttribute('watermark') ||
        el.hasAttribute('data-watermark') ||
        (el.classList && [...el.classList].some((cls) => /watermark/i.test(cls)))
      ) {
        return true;
      }

      const testId = el.getAttribute('data-testid');
      if (testId && /watermark/i.test(testId)) return true;

      const inline = el.getAttribute('style');
      if (
        inline &&
        (/(?:background|background-image)\s*:\s*url\(\s*data:image/i.test(inline) ||
          /(?:mask|mask-image|webkit-mask-image)\s*:\s*url\(\s*data:image/i.test(inline))
      ) {
        return true;
      }

      const computed = safeComputedStyle(el);
      if (computed) {
        const bg = computed.backgroundImage;
        if (bg && bg.includes('data:image')) return true;

        const mask = computed.maskImage || computed.webkitMaskImage;
        if (mask && mask.includes('data:image')) return true;
      }

      return false;
    };

    const clearZsxqWatermark = (el) => {
      if (!(el instanceof Element) || shouldSkipButton(el)) return;

      const inlineStyle = el.getAttribute('style');
      if (inlineStyle && /nullbackground/i.test(inlineStyle)) {
        el.setAttribute('style', inlineStyle.replace(/nullbackground/gi, 'background'));
      }

      const inlineBgImage = el.style.getPropertyValue('background-image');
      if (inlineBgImage && inlineBgImage !== 'none' && inlineBgImage.includes('url(')) {
        el.style.setProperty('background-image', 'none', 'important');
      }

      const inlineBg = el.style.getPropertyValue('background');
      if (inlineBg && inlineBg.includes('url(')) {
        el.style.setProperty(
          'background',
          inlineBg.replace(/url\([^)]*\)/gi, 'none').trim(),
          'important'
        );
      }

      const inlineMask =
        el.style.getPropertyValue('mask-image') ||
        el.style.getPropertyValue('-webkit-mask-image');
      if (inlineMask && inlineMask.includes('url(')) {
        el.style.setProperty('mask-image', 'none', 'important');
        el.style.setProperty('-webkit-mask-image', 'none', 'important');
      }

      const computed = safeComputedStyle(el);
      if (computed) {
        const computedBg = computed.backgroundImage;
        if (computedBg && computedBg !== 'none' && computedBg.includes('url(')) {
          el.style.setProperty('background-image', 'none', 'important');
        }
        const computedMask = computed.maskImage || computed.webkitMaskImage;
        if (computedMask && computedMask !== 'none' && computedMask.includes('url(')) {
          el.style.setProperty('mask-image', 'none', 'important');
          el.style.setProperty('-webkit-mask-image', 'none', 'important');
        }
      }
    };

    ensureObserver();

    whenBody(() => {
      attachBodyObserver();
      scanZsxqElements(document.body);
    });

    document.addEventListener('click', scheduleRescanAfterInteraction, true);
    document.addEventListener('keydown', scheduleRescanAfterInteraction, true);
    setInterval(() => {
      if (document.body) scanZsxqElements(document.body);
    }, 3000);
  }

  function injectZsxqCss() {
    if (document.getElementById(ZSXQ_STYLE_ID)) return;
    const style = document.createElement('style');
    style.id = ZSXQ_STYLE_ID;
    style.textContent = `
      [watermark],
      [data-watermark],
      [class*="watermark" i],
      [data-testid*="watermark" i] {
        background-image: none !important;
        mask-image: none !important;
        -webkit-mask-image: none !important;
      }
      [watermark]::before,
      [watermark]::after,
      [data-watermark]::before,
      [data-watermark]::after,
      [class*="watermark" i]::before,
      [class*="watermark" i]::after {
        background-image: none !important;
        mask-image: none !important;
        -webkit-mask-image: none !important;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  /* ========== “全屏水印层 + 图像特征”识别 ========== */

  function isGlobalOverlayWatermark(el, computed) {
    if (!(el instanceof Element)) return false;
    if (!computed) computed = safeComputedStyle(el);
    if (!computed) return false;

    const pos = computed.position;
    if (pos !== 'fixed' && pos !== 'absolute') return false;

    const fullScreen =
      (computed.top === '0px' || computed.top === '0') &&
      (computed.left === '0px' || computed.left === '0') &&
      (
        computed.bottom === '0px' ||
        computed.bottom === '0' ||
        computed.height === '100vh' ||
        computed.height === '100%' ||
        computed.height === 'auto'
      ) &&
      (
        computed.right === '0px' ||
        computed.right === '0' ||
        computed.width === '100vw' ||
        computed.width === '100%' ||
        computed.width === 'auto'
      );

    if (!fullScreen) return false;

    const z = parseInt(computed.zIndex, 10);
    if (isNaN(z) || z < 1000) return false;

    const bgImg = computed.backgroundImage;
    const bg = computed.background;
    const hasImg =
      (bgImg && bgImg !== 'none') ||
      (bg && /url\(/i.test(bg));

    if (!hasImg) return false;

    const pe = computed.pointerEvents;
    if (pe && pe !== 'none') return false;

    const tag = el.tagName;
    if (tag === 'HTML' || tag === 'BODY') return false;

    return true;
  }

  // 图像分析:计算水印评分
  function computeWatermarkScore(img) {
    const w = img.naturalWidth || img.width;
    const h = img.naturalHeight || img.height;
    if (!w || !h) return { score: 0, stats: null };

    const scale = Math.min(1, MAX_SAMPLE_SIZE / Math.max(w, h));
    const sw = Math.max(1, Math.floor(w * scale));
    const sh = Math.max(1, Math.floor(h * scale));

    const canvas = document.createElement('canvas');
    canvas.width = sw;
    canvas.height = sh;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0, sw, sh);

    const data = ctx.getImageData(0, 0, sw, sh).data;
    const totalPixels = sw * sh;

    let transparentCount = 0;
    let brightCount = 0;
    let darkCount = 0;
    let sumGray = 0;
    let sumGraySq = 0;

    for (let i = 0; i < data.length; i += 4) {
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      const a = data[i + 3];

      const gray = 0.299 * r + 0.587 * g + 0.114 * b;

      sumGray += gray;
      sumGraySq += gray * gray;

      if (a < 80) {
        transparentCount++;
      }

      if (gray > 220) brightCount++;
      else if (gray < 40) darkCount++;
    }

    const transparentRatio = transparentCount / totalPixels;
    const brightRatio = brightCount / totalPixels;
    const darkRatio = darkCount / totalPixels;

    const meanGray = sumGray / totalPixels;
    const varGray = sumGraySq / totalPixels - meanGray * meanGray;
    const contrast = Math.sqrt(Math.max(varGray, 0));

    let score = 0;
    if (transparentRatio > 0.5) score += 0.4;
    if (brightRatio > 0.3) score += 0.2;
    if (darkRatio < 0.1) score += 0.1;
    if (contrast < 60) score += 0.2;
    if (meanGray > 160) score += 0.1;

    return {
      score,
      stats: {
        transparentRatio,
        brightRatio,
        darkRatio,
        meanGray,
        contrast,
        width: w,
        height: h,
        sampleWidth: sw,
        sampleHeight: sh,
      },
    };
  }

  function analyzeImage(url) {
    return new Promise((resolve) => {
      const img = new Image();
      // 尝试跨域,失败了也只是拿不到像素
      img.crossOrigin = 'anonymous';
      img.onload = () => {
        try {
          const res = computeWatermarkScore(img);
          resolve({ url, ...res });
        } catch (e) {
          resolve({ url, score: 0, stats: null, error: e });
        }
      };
      img.onerror = () => resolve({ url, score: 0, stats: null, error: 'load-fail' });
      img.src = url;
    });
  }

  /* ========== 自动学习站点规则 ========== */

  function markElementProcessed(el) {
    if (!(el instanceof Element)) return;
    const prev = processedCountMap.get(el) || 0;
    const next = prev + 1;
    processedCountMap.set(el, next);

    if (next === HARD_KILL_THRESHOLD) {
      console.warn(
        `${LOG_PREFIX} 发现疑似持续自愈的水印元素,执行强制隐藏/移除:`,
        describeNode(el)
      );
      hardKillElement(el);
    }
  }

  function hardKillElement(el) {
    if (!(el instanceof Element)) return;
    try {
      el.classList.add('wm-remover-hard-kill');
    } catch (_) {}

    try {
      el.style.setProperty('display', 'none', 'important');
      el.style.setProperty('visibility', 'hidden', 'important');
      el.style.setProperty('opacity', '0', 'important');
      el.style.setProperty('background', 'none', 'important');
      el.style.setProperty('background-image', 'none', 'important');
      el.style.setProperty('mask-image', 'none', 'important');
      el.style.setProperty('-webkit-mask-image', 'none', 'important');
    } catch (_) {}

    try {
      if (el.parentNode) el.parentNode.removeChild(el);
      else if (typeof el.remove === 'function') el.remove();
    } catch (_) {}
  }

  function learnOverlayRulesFromCandidates(candidates) {
    const host = location.hostname;
    if (!candidates || !candidates.size) return;

    const cfg = loadSiteConfig(host);
    let added = 0;

    candidates.forEach((el) => {
      const rule = buildRuleFromElement(el);
      if (!rule) return;
      if (!cfg.rules.some((r) => r.selector === rule.selector)) {
        cfg.rules.push(rule);
        injectCssForRuleImmediate(rule);
        added++;
        console.info(`${LOG_PREFIX} 为站点 ${host} 新增自动识别规则:`, rule);
      }
    });

    if (added) {
      saveSiteConfig(host, cfg);
      console.info(`${LOG_PREFIX} 已保存 ${added} 条站点规则到存储`);
    }
  }

  function buildRuleFromElement(el) {
    if (!(el instanceof Element)) return null;

    let selector = null;
    if (el.classList && el.classList.length) {
      const cls = [...el.classList][0];
      selector = `html body .${cssEscape(cls)}`;
    } else if (el.id) {
      selector = `html body #${cssEscape(el.id)}`;
    } else {
      return null;
    }

    return {
      selector,
      props: [
        'background',
        'background-image',
        'mask-image',
        '-webkit-mask-image',
      ],
      note: 'auto-learned overlay watermark',
    };
  }

  function injectCssForRuleImmediate(rule) {
    let style = document.getElementById(SITE_RULES_STYLE_ID);
    if (!style) {
      style = document.createElement('style');
      style.id = SITE_RULES_STYLE_ID;
      (document.head || document.documentElement).appendChild(style);
    }
    style.textContent += cssFromRule(rule);
  }

  /* ========== 高风险清理:两级策略 ========== */
  // 第一次点击:smartHighRiskSweep(有限清理 + 图像特征筛选 + 学习规则)
  // 第二次及以后:bruteHighRiskSweep(无差别暴力)

  async function smartHighRiskSweep(root) {
    if (isHighRiskSweeping) return 0;
    isHighRiskSweeping = true;
    overlayCandidates = new Set();

    let processed = 0;
    const overlayImageTasks = []; // {el, url, computed, taskPromise}

    try {
      walkDom(root, (el) => {
        if (!(el instanceof Element) || shouldSkipButton(el)) return;

        const computed = safeComputedStyle(el);
        if (!computed) return;

        const bgImg = computed.backgroundImage;
        const bg = computed.background;
        let url = null;

        if (bgImg && bgImg !== 'none') {
          const m = /url\(["']?(.+?)["']?\)/.exec(bgImg);
          if (m) url = m[1];
        } else if (bg && /url\(/i.test(bg)) {
          const m2 = /url\(["']?(.+?)["']?\)/.exec(bg);
          if (m2) url = m2[1];
        }

        const isOverlay = isGlobalOverlayWatermark(el, computed);

        if (url && isOverlay) {
          // 暂不立即清理,先做内容特征分析
          overlayImageTasks.push({ el, url, computed });
        } else {
          // 普通元素,使用原来的“去背景”逻辑(较宽松)
          let changed = false;

          if (bgImg && bgImg !== 'none') {
            el.style.setProperty('background-image', 'none', 'important');
            changed = true;
          }
          if (bg && bg.includes('url(')) {
            el.style.setProperty(
              'background',
              bg.replace(/url\([^)]*\)/gi, 'none').trim(),
              'important'
            );
            changed = true;
          }

          const inlineMask =
            el.style.getPropertyValue('mask-image') ||
            el.style.getPropertyValue('-webkit-mask-image');
          if (inlineMask && inlineMask !== 'none') {
            el.style.setProperty('mask-image', 'none', 'important');
            el.style.setProperty('-webkit-mask-image', 'none', 'important');
            changed = true;
          }

          if (changed) {
            processed++;
            markElementProcessed(el);
          }
        }
      });

      // 对 overlayImageTasks 做图像分析,仅对评分高的进行清理 + 学习
      for (const { el, url, computed } of overlayImageTasks) {
        try {
          const { score, stats } = await analyzeImage(url);
          console.log(
            `${LOG_PREFIX} 分析背景图`, url, 'score=', score.toFixed(2), stats
          );
          if (score >= WATERMARK_SCORE_THRESHOLD) {
            el.style.setProperty('background', 'none', 'important');
            el.style.setProperty('background-image', 'none', 'important');
            processed++;
            markElementProcessed(el);
            overlayCandidates.add(el);
          } else {
            // 认为不是水印,暂时保留
          }
        } catch (e) {
          console.debug(`${LOG_PREFIX} 分析图片失败`, url, e);
        }
      }

    } finally {
      isHighRiskSweeping = false;
    }

    if (overlayCandidates.size > 0) {
      learnOverlayRulesFromCandidates(overlayCandidates);
    }

    return processed;
  }

  function bruteHighRiskSweep(root) {
    if (isHighRiskSweeping) return 0;
    isHighRiskSweeping = true;

    let processed = 0;
    try {
      walkDom(root, (el) => {
        if (!(el instanceof Element) || shouldSkipButton(el)) return;

        const computed = safeComputedStyle(el);
        let changed = false;

        if (computed) {
          const bg = computed.backgroundImage;
          if (bg && bg !== 'none') {
            el.style.setProperty('background-image', 'none', 'important');
            changed = true;
          }
          const mask = computed.maskImage || computed.webkitMaskImage;
          if (mask && mask !== 'none') {
            el.style.setProperty('mask-image', 'none', 'important');
            el.style.setProperty('-webkit-mask-image', 'none', 'important');
            changed = true;
          }
        }

        const inlineBg = el.style.getPropertyValue('background');
        if (inlineBg && inlineBg.includes('url(')) {
          el.style.setProperty(
            'background',
            inlineBg.replace(/url\([^)]*\)/gi, 'none').trim(),
            'important'
          );
          changed = true;
        }

        const inlineBgImage = el.style.getPropertyValue('background-image');
        if (inlineBgImage && inlineBgImage !== 'none') {
          el.style.setProperty('background-image', 'none', 'important');
          changed = true;
        }

        const inlineMask =
          el.style.getPropertyValue('mask-image') ||
          el.style.getPropertyValue('-webkit-mask-image');
        if (inlineMask && inlineMask !== 'none') {
          el.style.setProperty('mask-image', 'none', 'important');
          el.style.setProperty('-webkit-mask-image', 'none', 'important');
          changed = true;
        }

        if (changed) {
          processed++;
          markElementProcessed(el);
        }
      });
    } finally {
      isHighRiskSweeping = false;
    }
    return processed;
  }

  function enableAggressiveLoop(interval = 2000) {
    if (aggressiveTimer) return;
    aggressiveTimer = setInterval(() => {
      const root = document.body || document.documentElement;
      if (!root) return;
      const count = bruteHighRiskSweep(root);
      if (count > 0) {
        console.debug(`${LOG_PREFIX} 周期高风险清理:本次处理 ${count} 个元素`);
      }
    }, interval);
  }

  /* ========== 悬浮按钮:两级策略触发 ========== */
  function ensureSweepButton() {
    if (sweepButton) return;

    sweepButton = document.createElement('button');
    sweepButton.id = BUTTON_ID;
    sweepButton.type = 'button';
    sweepButton.textContent = '暴力去水印';
    sweepButton.title = '第一次点击使用智能识别(水印图片+全屏遮罩)有限清理并自动学习站点规则;若仍有残留,再点击则启用全面暴力清理。';

    applyButtonPlacement(buttonPos);

    sweepButton.addEventListener('pointerdown', onPointerDown);
    sweepButton.addEventListener('pointermove', onPointerMove);
    sweepButton.addEventListener('pointerup', onPointerUp);
    sweepButton.addEventListener('pointercancel', onPointerCancel);

    sweepButton.addEventListener('click', async (event) => {
      if (suppressClick) {
        event.stopPropagation();
        event.preventDefault();
        return;
      }

      highRiskClickCount += 1;
      const root = document.body || document.documentElement;
      const start = performance.now();

      if (highRiskClickCount === 1) {
        // 第一次:智能模式
        const count = await smartHighRiskSweep(root);
        const duration = (performance.now() - start).toFixed(1);
        console.info(
          `${LOG_PREFIX} 智能高风险清理:处理了 ${count} 个元素,用时 ${duration}ms(再次点击将触发全面暴力模式)`
        );
      } else {
        // 第二次及之后:纯暴力模式
        const count = bruteHighRiskSweep(root);
        const duration = (performance.now() - start).toFixed(1);
        console.info(
          `${LOG_PREFIX} 全量暴力清理:处理了 ${count} 个元素,用时 ${duration}ms`
        );
        enableAggressiveLoop(2000);
      }
    });

    whenBody(() => {
      document.body.appendChild(sweepButton);
    });
  }

  function onPointerDown(event) {
    if (!sweepButton) return;
    dragState.active = true;
    dragState.moved = false;
    dragState.pointerId = event.pointerId;
    dragState.startX = event.clientX;
    dragState.startY = event.clientY;
    try {
      sweepButton.setPointerCapture(event.pointerId);
    } catch (_) {}
  }

  function onPointerMove(event) {
    if (!dragState.active || !sweepButton || event.pointerId !== dragState.pointerId) return;

    const dx = event.clientX - dragState.startX;
    const dy = event.clientY - dragState.startY;

    if (!dragState.moved) {
      if (Math.hypot(dx, dy) > 4) {
        dragState.moved = true;
        sweepButton.classList.add('dragging');
      } else {
        return;
      }
    }

    event.preventDefault();

    const side = event.clientX >= window.innerWidth / 2 ? 'right' : 'left';
    applyButtonSide(side);

    const topRatio = clamp(event.clientY / window.innerHeight, 0.05, 0.95);
    applyButtonTop(topRatio);

    buttonPos = { side, top: topRatio };
  }

  function onPointerUp(event) {
    if (!dragState.active || !sweepButton || event.pointerId !== dragState.pointerId) return;
    try {
      sweepButton.releasePointerCapture(event.pointerId);
    } catch (_) {}

    if (dragState.moved) {
      event.preventDefault();
      saveButtonPosition(buttonPos);
      suppressClick = true;
      setTimeout(() => {
        suppressClick = false;
      }, 0);
    }

    sweepButton.classList.remove('dragging');
    dragState.active = false;
    dragState.moved = false;
    dragState.pointerId = null;
  }

  function onPointerCancel(event) {
    if (!dragState.active || !sweepButton || event.pointerId !== dragState.pointerId) return;
    try {
      sweepButton.releasePointerCapture(event.pointerId);
    } catch (_) {}
    sweepButton.classList.remove('dragging');
    dragState.active = false;
    dragState.moved = false;
    dragState.pointerId = null;
  }

  function applyButtonPlacement(pos) {
    applyButtonSide(pos.side);
    applyButtonTop(pos.top);
  }

  function applyButtonSide(side) {
    if (!sweepButton) return;
    if (side === 'right') {
      sweepButton.classList.add('side-right');
      sweepButton.classList.remove('side-left');
      sweepButton.style.left = 'auto';
      sweepButton.style.right = '0';
    } else {
      sweepButton.classList.add('side-left');
      sweepButton.classList.remove('side-right');
      sweepButton.style.left = '0';
      sweepButton.style.right = 'auto';
    }
    buttonPos.side = side === 'right' ? 'right' : 'left';
  }

  function applyButtonTop(topRatio) {
    if (!sweepButton) return;
    const clamped = clamp(topRatio, 0.05, 0.95);
    sweepButton.style.top = (clamped * 100).toFixed(2) + 'vh';
    buttonPos.top = clamped;
  }

  function injectBaseCss() {
    if (document.getElementById(BASE_STYLE_ID)) return;
    const style = document.createElement('style');
    style.id = BASE_STYLE_ID;
    style.textContent = `
      #${BUTTON_ID} {
        position: fixed;
        top: 50%;
        left: 0;
        transform: translate(-88%, -50%);
        padding: 11px 24px;
        border: none;
        border-radius: 0 18px 18px 0;
        font-size: 15px;
        font-weight: 700;
        letter-spacing: 0.08em;
        color: #ffffff;
        background: linear-gradient(135deg, #1d5fd7 0%, #0f3eb7 50%, #0d2a8e 100%);
        box-shadow: 0 16px 32px rgba(9, 40, 90, 0.45);
        text-shadow: 0 2px 3px rgba(0, 0, 0, 0.35);
        cursor: grab;
        z-index: 2147483646;
        opacity: 0.96;
        transition: transform 0.25s ease, opacity 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
        touch-action: none;
        user-select: none;
      }
      #${BUTTON_ID}.side-right {
        left: auto;
        right: 0;
        border-radius: 18px 0 0 18px;
        transform: translate(88%, -50%);
      }
      #${BUTTON_ID}.side-left:hover,
      #${BUTTON_ID}.side-left:focus-visible,
      #${BUTTON_ID}.side-left.dragging,
      #${BUTTON_ID}.side-right:hover,
      #${BUTTON_ID}.side-right:focus-visible,
      #${BUTTON_ID}.side-right.dragging {
        transform: translate(0, -50%);
        opacity: 1;
        box-shadow: 0 20px 36px rgba(9, 40, 90, 0.55);
      }
      #${BUTTON_ID}:active {
        background: linear-gradient(135deg, #184fc0 0%, #0c3296 100%);
        box-shadow: 0 12px 28px rgba(9, 40, 90, 0.5);
        cursor: grabbing;
      }
      #${BUTTON_ID}.dragging {
        transition: none;
      }
      #${BUTTON_ID}:focus,
      #${BUTTON_ID}:focus-visible {
        outline: none;
      }
      #${BUTTON_ID}::after {
        content: '⟲';
        margin-left: 10px;
        font-size: 14px;
        text-shadow: inherit;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  function injectHardKillCss() {
    if (document.getElementById(HARD_KILL_STYLE_ID)) return;
    const style = document.createElement('style');
    style.id = HARD_KILL_STYLE_ID;
    style.textContent = `
      .wm-remover-hard-kill {
        display: none !important;
        visibility: hidden !important;
        opacity: 0 !important;
        background: none !important;
        background-image: none !important;
        mask-image: none !important;
        -webkit-mask-image: none !important;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  function injectGlobalAggressiveCss() {
    if (document.getElementById(GLOBAL_AGGRESSIVE_STYLE_ID)) return;
    const style = document.createElement('style');
    style.id = GLOBAL_AGGRESSIVE_STYLE_ID;
    style.textContent = `
      [watermark],
      [data-watermark],
      [class*="watermark" i],
      [data-testid*="watermark" i] {
        background: none !important;
        background-image: none !important;
        mask-image: none !important;
        -webkit-mask-image: none !important;
      }
      [watermark]::before,
      [watermark]::after,
      [data-watermark]::before,
      [data-watermark]::after,
      [class*="watermark" i]::before,
      [class*="watermark" i]::after {
        background: none !important;
        background-image: none !important;
        mask-image: none !important;
        -webkit-mask-image: none !important;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  /* ========== “还原行为”检测逻辑(原有 1.6 保留) ========== */
  function setupRestoreDetection() {
    try {
      patchMutationObserver();
      patchSetAttribute();
      setupWatermarkMutationLogger();
      setupInteractionCorrelator();
    } catch (e) {
      console.debug(`${LOG_PREFIX} 还原检测逻辑初始化失败:`, e);
    }
  }

  function patchMutationObserver() {
    if (!window.MutationObserver) return;
    const NativeMO = window.MutationObserver;

    window.MutationObserver = function WrappedMutationObserver(callback) {
      const wrappedCallback = function (mutations, observer) {
        if (mutations && mutations.length) {
          for (const m of mutations) {
            if (mutationHasWatermark(m)) {
              console.log(
                `${LOG_PREFIX} 检测到 MutationObserver 回调中涉及 "watermark" 相关节点:`,
                {
                  type: m.type,
                  target: describeNode(m.target),
                }
              );
              break;
            }
          }
        }
        return callback(mutations, observer);
      };

      console.log(
        `${LOG_PREFIX} 页面创建 MutationObserver 实例,可能用于自愈布局或水印。`
      );
      return new NativeMO(wrappedCallback);
    };
    window.MutationObserver.prototype = NativeMO.prototype;
  }

  function mutationHasWatermark(m) {
    try {
      if (!m) return false;
      const checkEl = (el) =>
        el &&
        el.nodeType === 1 &&
        (isWatermarkLike(el) ||
          /watermark/i.test(el.className || '') ||
          /watermark/i.test(el.id || ''));
      if (m.type === 'attributes') {
        return /watermark/i.test(m.attributeName || '') || checkEl(m.target);
      } else if (m.type === 'childList') {
        for (const n of m.addedNodes || []) {
          if (n.nodeType === 1 && checkEl(n)) return true;
        }
      }
    } catch (_) {}
    return false;
  }

  function patchSetAttribute() {
    const nativeSetAttribute = Element.prototype.setAttribute;
    Element.prototype.setAttribute = function (name, value) {
      if (
        typeof name === 'string' &&
        (/watermark/i.test(name) || /watermark/i.test(String(value)))
      ) {
        console.log(
          `${LOG_PREFIX} 检测到 setAttribute 可能还原水印:`,
          {
            name,
            value,
            node: describeNode(this),
          }
        );
      }
      return nativeSetAttribute.call(this, name, value);
    };
  }

  function setupWatermarkMutationLogger() {
    if (!window.MutationObserver) return;
    const logger = new MutationObserver((mutations) => {
      for (const m of mutations) {
        if (mutationHasWatermark(m)) {
          console.log(
            `${LOG_PREFIX} 监测到疑似水印相关 DOM 变更:`,
            {
              type: m.type,
              attributeName: m.attributeName,
              target: describeNode(m.target),
            }
          );
        }
      }
    });
    const start = () => {
      if (!document.body) return false;
      logger.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style', 'class', 'watermark', 'data-watermark', 'data-testid'],
      });
      return true;
    };
    if (!start()) {
      const watcher = new MutationObserver(() => {
        if (start()) watcher.disconnect();
      });
      watcher.observe(document.documentElement, { childList: true });
    }
  }

  function setupInteractionCorrelator() {
    let lastInteraction = null;

    const markInteraction = (type, extra) => {
      lastInteraction = {
        type,
        extra,
        time: performance.now(),
      };
    };

    const interactionTypes = ['click', 'keydown', 'scroll', 'resize'];
    interactionTypes.forEach((evt) => {
      window.addEventListener(
        evt,
        (e) => {
          markInteraction(evt, describeEvent(e));
        },
        true
      );
    });

    if (!window.MutationObserver) return;
    const mo = new MutationObserver((mutations) => {
      if (!lastInteraction) return;
      const now = performance.now();
      if (now - lastInteraction.time > 500) return;

      for (const m of mutations) {
        if (mutationHasWatermark(m)) {
          console.log(
            `${LOG_PREFIX} 检测到交互后疑似“水印还原”行为:`,
            {
              interaction: lastInteraction,
              mutation: {
                type: m.type,
                attributeName: m.attributeName,
                target: describeNode(m.target),
              },
            }
          );
          break;
        }
      }
    });

    const start = () => {
      if (!document.body) return false;
      mo.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style', 'class', 'watermark', 'data-watermark', 'data-testid'],
      });
      return true;
    };
    if (!start()) {
      const watcher = new MutationObserver(() => {
        if (start()) watcher.disconnect();
      });
      watcher.observe(document.documentElement, { childList: true });
    }
  }

  /* ========== 状态 & 工具函数 ========== */
  function loadButtonPosition() {
    const fallback = { side: 'left', top: 0.5 };
    try {
      if (typeof GM_getValue === 'function') {
        const stored = GM_getValue(BUTTON_STORAGE_KEY);
        if (stored && typeof stored === 'object') {
          return normalizeButtonPos(stored, fallback);
        }
      } else if (window.localStorage) {
        const raw = window.localStorage.getItem(BUTTON_STORAGE_KEY);
        if (raw) {
          return normalizeButtonPos(JSON.parse(raw), fallback);
        }
      }
    } catch (err) {
      console.debug(`${LOG_PREFIX} 读取按钮位置失败:`, err);
    }
    return { ...fallback };
  }

  function saveButtonPosition(pos) {
    const normalized = normalizeButtonPos(pos, { side: 'left', top: 0.5 });
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue(BUTTON_STORAGE_KEY, normalized);
      } else if (window.localStorage) {
        window.localStorage.setItem(BUTTON_STORAGE_KEY, JSON.stringify(normalized));
      }
    } catch (err) {
      console.debug(`${LOG_PREFIX} 保存按钮位置失败:`, err);
    }
  }

  function normalizeButtonPos(pos, fallback) {
    if (!pos || typeof pos !== 'object') return { ...fallback };
    const side = pos.side === 'right' ? 'right' : 'left';
    const top = clamp(typeof pos.top === 'number' ? pos.top : fallback.top, 0.05, 0.95);
    return { side, top };
  }

  function walkDom(root, cb) {
    if (!root) return;
    if (root instanceof Element) {
      cb(root);
      if (root.shadowRoot) walkDom(root.shadowRoot, cb);
      for (const child of root.children) {
        walkDom(child, cb);
      }
    } else if (
      root instanceof DocumentFragment ||
      root instanceof ShadowRoot ||
      root instanceof Document
    ) {
      const nodes = root.children || root.childNodes;
      for (const child of nodes) {
        if (child.nodeType === 1) walkDom(child, cb);
      }
    }
  }

  function shouldSkipButton(el) {
    return sweepButton && (el === sweepButton || sweepButton.contains(el));
  }

  function clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
  }

  function whenReady(fn) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', fn, { once: true });
    } else {
      fn();
    }
  }

  function whenBody(fn) {
    if (document.body) {
      fn();
    } else {
      const watcher = new MutationObserver(() => {
        if (document.body) {
          watcher.disconnect();
          fn();
        }
      });
      watcher.observe(document.documentElement, { childList: true });
    }
  }

  function safeComputedStyle(el) {
    try {
      return window.getComputedStyle(el);
    } catch {
      return null;
    }
  }

  function describeNode(node) {
    try {
      if (!node || node.nodeType !== 1) return null;
      const el = node;
      return {
        tag: el.tagName,
        id: el.id || undefined,
        class: el.className || undefined,
        attrs: {
          watermark: el.getAttribute('watermark') || undefined,
          dataWatermark: el.getAttribute('data-watermark') || undefined,
          dataTestid: el.getAttribute('data-testid') || undefined,
        },
      };
    } catch {
      return null;
    }
  }

  function describeEvent(e) {
    if (!e) return null;
    const base = { type: e.type, timeStamp: e.timeStamp };
    if (e.type === 'click') {
      return {
        ...base,
        x: e.clientX,
        y: e.clientY,
        target: describeNode(e.target),
      };
    }
    if (e.type === 'keydown') {
      return {
        ...base,
        key: e.key,
        code: e.code,
      };
    }
    if (e.type === 'scroll') {
      return {
        ...base,
        scrollX: window.scrollX,
        scrollY: window.scrollY,
      };
    }
    if (e.type === 'resize') {
      return {
        ...base,
        innerWidth: window.innerWidth,
        innerHeight: window.innerHeight,
      };
    }
    return base;
  }
})();