Enable Select & Copy — Multi-Level (L1–L6)

多等级解锁复制:默认最保守(等级1),可逐级增强到等级6;为每站点记忆;菜单与快捷键快速升降;尽量降低被风控检测的概率

// ==UserScript==
// @name         Enable Select & Copy — Multi-Level (L1–L6)
// @namespace    tm-copy-unlock
// @version      3.1
// @description  多等级解锁复制:默认最保守(等级1),可逐级增强到等级6;为每站点记忆;菜单与快捷键快速升降;尽量降低被风控检测的概率
// @match        *://*/*
// @run-at       document-start
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  /***************************************************************************
   * 总览
   * L1 纯样式                   —— 最安全,仅 user-select:text
   * L2 复制瞬间放行             —— 监听 Ctrl/⌘+C,临时宽松 copy
   * L3 选择与右键修复           —— 少量事件捕获拦截 + 根节点清理 inline
   * L4 动态节点定向修复         —— Scoped MutationObserver + 定向容器
   * L5 全局防御(激进)         —— 全站事件捕获 + 全树扫描 + 全站 MO + Shadow
   * L6 复制瞬间直接写剪贴板     —— 不修页面,只在复制瞬间写 clipboard(兜底)
   ***************************************************************************/

  // ===== 0) 配置 & 存储 Key =====
  const DEFAULT_LEVEL = 1; // 全局默认等级(1 最保守)
  const STORAGE = {
    SITE_LEVEL_PREFIX: 'copyUnlock.level.', // + hostname => 1..6
    HOTKEY_ENABLED: 'copyUnlock.hotkeyEnabled', // 是否启用快捷键
    DIAG_ENABLED: 'copyUnlock.diagEnabled', // 轻量诊断日志
  };

  // 可按需调整:用于 L4 的“定向容器”白名单(仅对这些容器子树做修复)
  const L4_SCOPE_SELECTORS = [
    'article', 'main', '#main', '.content', '.post', '.article', '.RichContent', '[role="article"]'
  ];

  const host = location.hostname;
  const SITE_KEY = STORAGE.SITE_LEVEL_PREFIX + host;

  // 读取/保存
  const read = (k, d) => {
    const v = GM_getValue(k);
    return v === undefined ? d : v;
  };
  const save = (k, v) => GM_setValue(k, v);

  let currentLevel = clamp(read(SITE_KEY, DEFAULT_LEVEL), 1, 6);
  let hotkeyEnabled = read(STORAGE.HOTKEY_ENABLED, true);
  let diag = read(STORAGE.DIAG_ENABLED, false);

  function clamp(n, a, b) { return Math.max(a, Math.min(b, n | 0)); }

  // ===== 1) 轻量日志 & 提示 =====
  function log(...args) { if (diag) console.log('[CopyUnlock]', ...args); }
  function toast(msg) {
    try { alert(msg); } catch { console.log('[CopyUnlock][toast]', msg); }
  }

  // ===== 2) 诊断探针(可选,超轻量)=====
  const CopyProbe = (() => {
    let lastCopySuccess = null; // true/false/null
    let recent = []; // 最近结果
    function note(ok) {
      lastCopySuccess = !!ok;
      recent.push({ t: Date.now(), ok: !!ok });
      if (recent.length > 10) recent.shift();
      log('Copy result:', ok);
    }
    function stats() {
      const total = recent.length;
      const ok = recent.filter(x => x.ok).length;
      return { total, ok, fail: total - ok, lastCopySuccess };
    }
    return { note, stats, last: () => lastCopySuccess };
  })();

  // ===== 3) 工具 =====
  function isMac() { return navigator.platform && /mac/i.test(navigator.platform); }
  function isCopyCombo(e) {
    const meta = isMac() ? e.metaKey : e.ctrlKey;
    return meta && (e.key === 'c' || e.key === 'C');
  }
  function addCSS(css, root = document.documentElement) {
    if (typeof GM_addStyle === 'function' && root === document.documentElement) {
      GM_addStyle(css);
      return;
    }
    const st = document.createElement('style');
    st.textContent = css;
    (root.shadowRoot ? root.shadowRoot : root).appendChild(st);
  }

  // 读取选区文本
  function getSelectionText() {
    try { return String(window.getSelection ? window.getSelection().toString() : ''); }
    catch { return ''; }
  }

  // 尝试写剪贴板(优先 clipboard.writeText,退回 execCommand)
  async function writeClipboard(text) {
    if (!text) return false;
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(text);
        return true;
      }
    } catch (e) { log('clipboard.writeText failed', e); }
    try {
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.setAttribute('readonly', 'readonly');
      ta.style.position = 'fixed';
      ta.style.left = '-9999px';
      document.body.appendChild(ta);
      ta.select();
      const ok = document.execCommand('copy');
      document.body.removeChild(ta);
      return !!ok;
    } catch (e) {
      log('execCommand(copy) failed', e);
      return false;
    }
  }

  // 仅在捕获阶段“拦链不拦默认”(用于 L3/L5)
  function makeStopper(allowCtrlC = true) {
    return function stopper(e) {
      if (allowCtrlC && e.type === 'keydown' && isCopyCombo(e)) return; // 放行原生复制
      e.stopImmediatePropagation();
    };
  }

  // 基础清理(少量)
  const INLINE_ATTRS = ['oncopy', 'oncut', 'oncontextmenu', 'onselectstart', 'ondragstart', 'onbeforecopy', 'onkeydown'];
  function cleanNodeLight(el) {
    if (!el || el.nodeType !== 1) return;
    for (const a of INLINE_ATTRS) {
      if (a in el) { try { el[a] = null; } catch {} }
      if (el.hasAttribute && el.hasAttribute(a)) el.removeAttribute(a);
    }
    const s = el.style;
    if (s) {
      try {
        s.setProperty('user-select', 'text', 'important');
        s.setProperty('-webkit-user-select', 'text', 'important');
      } catch {}
    }
  }

  function scanTreeLight(root) {
    cleanNodeLight(root);
    const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
    let n; while ((n = it.nextNode())) cleanNodeLight(n);
  }

  // ===== 4) 各等级实现 =====
  const Levels = {
    1() {
      // 纯样式(最安全)
      addCSS(`
        html, body, * {
          -webkit-user-select: text !important;
          -moz-user-select: text !important;
          -ms-user-select: text !important;
          user-select: text !important;
        }
      `);
      log('L1 enabled');
    },

    2() {
      // 复制瞬间临时放行:仅在按下 Ctrl/⌘+C 时,尽量保证复制能成功
      Levels[1](); // 保留 L1 的样式
      const onKeyDown = (e) => {
        if (!isCopyCombo(e)) return;
        // 短暂“疏通” copy 事件链:在捕获阶段抢先阻断站点监听
        const tempStopper = (ev) => { ev.stopImmediatePropagation(); };
        document.addEventListener('copy', tempStopper, true);
        setTimeout(() => {
          document.removeEventListener('copy', tempStopper, true);
        }, 100); // 瞬时生效

        // 同时兜底:如果站点仍阻止,我们手工写剪贴板(不破坏默认)
        setTimeout(async () => {
          const text = getSelectionText();
          if (!text) return;
          // 检测是否已经复制成功:无法可靠检测,只做兜底写入
          const ok = await writeClipboard(text);
          CopyProbe.note(ok);
        }, 0);
      };
      window.addEventListener('keydown', onKeyDown, true);
      log('L2 enabled');
    },

    3() {
      // 选择与右键修复(中等):少量事件捕获 + 根节点清理 inline
      Levels[1]();
      const stopper = makeStopper(true);
      ['selectstart', 'contextmenu', 'copy'].forEach(t => {
        document.addEventListener(t, stopper, true);
        window.addEventListener(t, stopper, true);
      });
      // 根节点轻量清理一次
      cleanNodeLight(document.documentElement);
      cleanNodeLight(document.body);
      log('L3 enabled');
    },

    4() {
      // 动态节点定向修复:Scoped MO + 指定容器
      Levels[3]();
      const scopeNodes = [];
      const pushIf = (el) => { if (el) scopeNodes.push(el); };

      for (const sel of L4_SCOPE_SELECTORS) {
        try { pushIf(document.querySelector(sel)); } catch {}
      }
      // 如果没匹配到,就退回 body(仍算“定向”,别全站乱扫)
      if (!scopeNodes.length) pushIf(document.body);

      const mo = new MutationObserver(muts => {
        for (const m of muts) {
          if (m.type === 'attributes') cleanNodeLight(m.target);
          else if (m.addedNodes && m.addedNodes.length) {
            m.addedNodes.forEach(n => { if (n.nodeType === 1) scanTreeLight(n); });
          }
        }
      });
      scopeNodes.forEach(node => {
        try { mo.observe(node, { subtree: true, childList: true, attributes: true }); } catch {}
      });
      log('L4 enabled on scopes:', scopeNodes);
    },

    5() {
      // 全局防御(激进):全站事件捕获 + 初始化全树清理 + 全站 MO + Shadow 样式注入
      Levels[1]();

      const stopper = makeStopper(true);
      const BLOCKED = ['copy','cut','contextmenu','selectstart','dragstart','beforecopy','keydown'];
      BLOCKED.forEach(t => {
        document.addEventListener(t, stopper, true);
        window.addEventListener(t, stopper, true);
      });

      // 初始化全树清理(谨慎:只做一次)
      try { scanTreeLight(document.documentElement); } catch {}

      // 全站观察(注意:可能影响性能,若页面很重可改成节流/选择器限定)
      const mo = new MutationObserver(muts => {
        for (const m of muts) {
          if (m.type === 'attributes') cleanNodeLight(m.target);
          else if (m.addedNodes && m.addedNodes.length) {
            m.addedNodes.forEach(n => { if (n.nodeType === 1) scanTreeLight(n); });
          }
        }
      });
      try { mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true }); } catch {}

      // Shadow DOM:不覆写原型,不改函数签名。仅“被动探测已存在的 shadowRoot”并注入样式
      const shadowMO = new MutationObserver(muts => {
        for (const m of muts) {
          if (m.addedNodes) {
            m.addedNodes.forEach(n => {
              try {
                if (n.shadowRoot) {
                  addCSS(`
                    :host, * {
                      -webkit-user-select: text !important;
                      user-select: text !important;
                    }
                  `, n.shadowRoot);
                  // 在 shadowRoot 内也拦常见事件(捕获)
                  const s2 = makeStopper(true);
                  ['copy','contextmenu','selectstart'].forEach(t => n.shadowRoot.addEventListener(t, s2, true));
                }
              } catch {}
            });
          }
        }
      });
      try { shadowMO.observe(document.documentElement, { subtree: true, childList: true }); } catch {}

      log('L5 enabled');
    },

    6() {
      // 复制瞬间直接写剪贴板(兜底):不解锁右键/选择;仅在 Ctrl/⌘+C 时写剪贴板
      const onKeyDown = async (e) => {
        if (!isCopyCombo(e)) return;
        const text = getSelectionText();
        if (!text) return;
        // 主动拦下默认 copy,直接写入,避免被站点逻辑“洗稿/插尾注”
        e.preventDefault();
        const ok = await writeClipboard(text);
        CopyProbe.note(ok);
      };
      window.addEventListener('keydown', onKeyDown, true);
      log('L6 enabled');
    },
  };

  // ===== 5) 运行入口 =====
  function bootstrap(level) {
    log(`Boot at level ${level} for ${host}`);
    // 分等级延迟:某些站点初始化期间会做完整性检查,低等级不需延迟
    const delayByLevel = { 1: 0, 2: 0, 3: 0, 4: 200, 5: 600, 6: 0 };
    const run = () => {
      try { Levels[level](); } catch (e) { console.error('[CopyUnlock] init error', e); }
    };
    if (document.readyState === 'loading') {
      // 对于 L5/L4 可等 DOM 基本完成后再注入,降低被检概率
      const dly = delayByLevel[level] || 0;
      if (dly > 0) {
        document.addEventListener('DOMContentLoaded', () => setTimeout(run, dly), { once: true });
      } else {
        document.addEventListener('DOMContentLoaded', run, { once: true });
      }
    } else {
      const dly = delayByLevel[level] || 0;
      if (dly > 0) setTimeout(run, dly);
      else run();
    }
  }

  // ===== 6) 菜单 & 快捷键 =====
  function setLevel(lv, silent = false) {
    currentLevel = clamp(lv, 1, 6);
    save(SITE_KEY, currentLevel);
    if (!silent) toast(`已设置本站等级为:L${currentLevel}(${host})\n刷新页面后生效。`);
  }
  function incLevel() { setLevel(currentLevel + 1); }
  function decLevel() { setLevel(currentLevel - 1); }
  function resetLevel() { setLevel(DEFAULT_LEVEL); }

  GM_registerMenuCommand(`当前等级:L${currentLevel}(点击查看说明)`, () => {
    toast(
      `等级说明(越高越强,越易触发风控):
L1 纯样式(最安全)
L2 复制瞬间放行
L3 选择/右键修复(少量事件捕获)
L4 动态节点定向修复(Scoped MO)
L5 全局防御(激进)
L6 复制瞬间直写剪贴板(兜底)

建议:从 L1 开始,遇阻再升;如遇异常/风控,立即降级或切 L6。`
    );
  }, { autoClose: true });

  GM_registerMenuCommand('升一级 (L↑)', () => incLevel(), { autoClose: true });
  GM_registerMenuCommand('降一级 (L↓)', () => decLevel(), { autoClose: true });
  GM_registerMenuCommand('重置为默认等级 (L1)', () => resetLevel(), { autoClose: true });

  GM_registerMenuCommand(`${hotkeyEnabled ? '关闭' : '开启'} 快捷键 (Alt+Shift+↑/↓/0)`, () => {
    hotkeyEnabled = !hotkeyEnabled;
    save(STORAGE.HOTKEY_ENABLED, hotkeyEnabled);
    toast(`快捷键已${hotkeyEnabled ? '开启' : '关闭'}`);
  }, { autoClose: true });

  GM_registerMenuCommand(`${diag ? '关闭' : '开启'} 诊断日志`, () => {
    diag = !diag;
    save(STORAGE.DIAG_ENABLED, diag);
    toast(`诊断日志已${diag ? '开启' : '关闭'}`);
  }, { autoClose: true });

  // 快捷键:Alt+Shift+↑/↓ 调整等级;Alt+Shift+0 复位
  window.addEventListener('keydown', (e) => {
    if (!hotkeyEnabled) return;
    if (!e.altKey || !e.shiftKey) return;
    const k = e.key;
    if (k === 'ArrowUp') { e.preventDefault(); incLevel(); }
    else if (k === 'ArrowDown') { e.preventDefault(); decLevel(); }
    else if (k === '0' || k === ')') { e.preventDefault(); resetLevel(); }
  }, true);

  // ===== 7) 启动当前等级 =====
  bootstrap(currentLevel);

  // ===== 8) 合规提醒(一次性提示,可按需保留/删除)=====
  // console.warn('[CopyUnlock] 请在合法、合规且尊重版权的前提下使用本脚本。');
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。