您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
解除网页复制/选中/右键限制;支持白/黑名单模式及切换、站点级覆盖、菜单管理清单、快捷键总开关与 Alt+Shift+C 切换
当前为
// ==UserScript== // @name Enable Select & Copy — WL/BL + Mode Switch + Hotkey // @namespace tm-copy-unlock // @version 3.0 // @description 解除网页复制/选中/右键限制;支持白/黑名单模式及切换、站点级覆盖、菜单管理清单、快捷键总开关与 Alt+Shift+C 切换 // @match *://*/* // @run-at document-start // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function () { 'use strict'; // ========== 0) 默认配置(可按需改) ========== // 默认白名单(仅在这些域名上启用;支持 *.domain.com) const DEFAULT_WHITELIST = [ '*.zhihu.com', '*.jianshu.com', '*.csdn.net', // 'example.com', ]; // 默认黑名单(黑名单模式下,这些域名禁用) const DEFAULT_BLACKLIST = [ // '*.some-interactive-site.com', ]; // ========== 1) 常量 & 存储 Key ========== const STORAGE = { MODE: 'copyUnlock.mode', // 'whitelist' | 'blacklist' WL: 'copyUnlock.whitelist', BL: 'copyUnlock.blacklist', SITE_OVERRIDE_PREFIX: 'copyUnlock.site.', // + hostname => true/false/null HOTKEY_ENABLED: 'copyUnlock.hotkeyEnabled', }; // ========== 2) 工具函数 ========== const host = location.hostname; function arr(val, fallback = []) { return Array.isArray(val) ? val : fallback; } function unique(a) { return Array.from(new Set(a)); } function save(key, value) { GM_setValue(key, value); } function read(key, fallback) { const v = GM_getValue(key); return v === undefined ? fallback : v; } function escRegex(s) { return s.replace(/[.+^${}()|[\]\\]/g, '\\$&'); } function globToReg(glob) { return new RegExp('^' + glob.split('*').map(escRegex).join('.*') + '$', 'i'); } function matchAny(hostname, globs) { const regs = globs.map(globToReg); return regs.some(r => r.test(hostname)); } // 将当前站点标准化为通配:三级及以上 => *.domain.tld;否则原样 function currentHostGlob() { const parts = host.split('.'); if (parts.length >= 3) return '*.' + parts.slice(-2).join('.'); return host; } function toast(msg) { // 轻提示:避免页面没权限时 alert 阻塞 try { console.log('[CopyUnlock]', msg); alert(msg); } catch { /* eslint-disable no-console */ console.log('[CopyUnlock]', msg); } } // ========== 3) 读取 / 合并清单 & 模式 ========== const persistedWL = arr(read(STORAGE.WL, [])); const persistedBL = arr(read(STORAGE.BL, [])); const WL = unique([...(DEFAULT_WHITELIST || []), ...persistedWL]); const BL = unique([...(DEFAULT_BLACKLIST || []), ...persistedBL]); let mode = read(STORAGE.MODE, 'whitelist'); // 'whitelist' | 'blacklist' (默认白名单) if (mode !== 'whitelist' && mode !== 'blacklist') { mode = 'whitelist'; save(STORAGE.MODE, mode); } // 站点覆盖:true=强制开, false=强制关, null/undefined=按模式 const SITE_KEY = STORAGE.SITE_OVERRIDE_PREFIX + host; let siteOverride = read(SITE_KEY, null); // 快捷键总开关 let hotkeyEnabled = read(STORAGE.HOTKEY_ENABLED, true); // 根据模式决定是否启用 const inWL = matchAny(host, WL); const inBL = matchAny(host, BL); function calcEffectiveOn() { if (siteOverride === true) return true; if (siteOverride === false) return false; if (mode === 'whitelist') return inWL; // 黑名单模式:只要不在黑名单,就启用 return !inBL; } let effectiveOn = calcEffectiveOn(); // ========== 4) 菜单:模式切换、站点覆盖、清单管理、快捷键总开关 ========== function toggleMode() { mode = (mode === 'whitelist') ? 'blacklist' : 'whitelist'; save(STORAGE.MODE, mode); effectiveOn = calcEffectiveOn(); toast(`已切换为【${mode === 'whitelist' ? '白名单' : '黑名单'}】模式。\n当前站点(${host})状态:${effectiveOn ? '启用' : '禁用'}`); } function setSiteOverride(val /* true/false/null */) { siteOverride = (val === null) ? null : !!val; if (val === null) { // 清除覆盖 save(SITE_KEY, null); effectiveOn = calcEffectiveOn(); toast(`已清除当前站点覆盖设定(${host})。\n按模式:${mode === 'whitelist' ? '白名单' : '黑名单'} → ${effectiveOn ? '启用' : '禁用'}`); } else { save(SITE_KEY, siteOverride); effectiveOn = calcEffectiveOn(); toast(`已${siteOverride ? '开启' : '关闭'} 当前站点解锁复制(${host})。`); } } function addToList(key, listArr, glob) { if (!glob) glob = currentHostGlob(); listArr.push(glob); save(key, unique(listArr)); toast(`已加入:${glob}\n列表:${key}`); } function removeFromList(key, listArr, glob) { if (!glob) glob = currentHostGlob(); const idx = listArr.indexOf(glob); if (idx >= 0) { listArr.splice(idx, 1); save(key, unique(listArr)); toast(`已移除:${glob}\n列表:${key}`); } else { toast(`未找到:${glob}\n列表:${key}`); } } GM_registerMenuCommand( `当前模式:${mode === 'whitelist' ? '白名单' : '黑名单'}(点击切换)`, toggleMode, { autoClose: true } ); GM_registerMenuCommand( `${effectiveOn ? '关闭' : '开启'} 当前站点解锁复制`, () => setSiteOverride(!effectiveOn), { autoClose: true } ); GM_registerMenuCommand( '清除当前站点覆盖(恢复按模式判断)', () => setSiteOverride(null), { autoClose: true } ); GM_registerMenuCommand( '将当前站点加入白名单', () => addToList(STORAGE.WL, persistedWL, currentHostGlob()), { autoClose: true } ); GM_registerMenuCommand( '将当前站点移出白名单', () => removeFromList(STORAGE.WL, persistedWL, currentHostGlob()), { autoClose: true } ); GM_registerMenuCommand( '将当前站点加入黑名单', () => addToList(STORAGE.BL, persistedBL, currentHostGlob()), { autoClose: true } ); GM_registerMenuCommand( '将当前站点移出黑名单', () => removeFromList(STORAGE.BL, persistedBL, currentHostGlob()), { autoClose: true } ); GM_registerMenuCommand( '查看白/黑名单', () => { const curGlob = currentHostGlob(); toast( `当前站点通配:${curGlob}\n\n白名单:\n${JSON.stringify(unique(WL), null, 2)}\n\n黑名单:\n${JSON.stringify(unique(BL), null, 2)}` ); }, { autoClose: true } ); GM_registerMenuCommand( `${hotkeyEnabled ? '关闭' : '开启'} 快捷键 (Alt+Shift+C)`, () => { hotkeyEnabled = !hotkeyEnabled; save(STORAGE.HOTKEY_ENABLED, hotkeyEnabled); toast(`快捷键已${hotkeyEnabled ? '开启' : '关闭'}。`); }, { autoClose: true } ); // 快捷键:Alt+Shift+C → 切换当前站点覆盖(开/关) window.addEventListener( 'keydown', e => { if (!hotkeyEnabled) return; if (e.altKey && e.shiftKey && (e.key === 'c' || e.key === 'C')) { e.preventDefault(); setSiteOverride(!effectiveOn); } }, true ); // 如果当前不启用,直接退出(保留菜单) if (!effectiveOn) return; // ========== 5) 解锁复制逻辑 ========== const BLOCKED = new Set(['copy', 'cut', 'contextmenu', 'selectstart', 'dragstart', 'beforecopy', 'keydown']); // 允许选中/右键 等 const css = ` html, body, * { -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; } *[unselectable="on"] { -webkit-user-select: text !important; user-select: text !important; } `; if (typeof GM_addStyle === 'function') { GM_addStyle(css); } else { const st = document.createElement('style'); st.textContent = css; document.documentElement.appendChild(st); } // 捕获阶段拦截站点绑定的“禁复制”监听(不破坏浏览器默认复制) const stopper = e => { if (e.type === 'keydown') { const isMac = navigator.platform.toUpperCase().includes('MAC'); const meta = isMac ? e.metaKey : e.ctrlKey; if (meta && (e.key === 'c' || e.key === 'C')) return; // 放行 Ctrl/⌘+C } // 不 preventDefault,只阻止站点自己的监听链 e.stopImmediatePropagation(); }; for (const t of BLOCKED) { window.addEventListener(t, stopper, true); document.addEventListener(t, stopper, true); } // 清理常见 inline 阻断 + 强制可选 const inlineAttrs = ['oncopy', 'oncut', 'oncontextmenu', 'onselectstart', 'ondragstart', 'onbeforecopy', 'onkeydown']; function cleanNode(el) { if (!el || el.nodeType !== 1) return; for (const a of inlineAttrs) { if (a in el) { try { el[a] = null; } catch {} } if (el.hasAttribute && el.hasAttribute(a)) { el.removeAttribute(a); } } if (el.getAttribute && el.getAttribute('unselectable') === 'on') { el.removeAttribute('unselectable'); } if (el.style) { try { el.style.setProperty('user-select', 'text', 'important'); el.style.setProperty('-webkit-user-select', 'text', 'important'); } catch {} } } function scanTree(root) { cleanNode(root); const it = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT); let n; while ((n = it.nextNode())) cleanNode(n); } const mo = new MutationObserver(muts => { for (const m of muts) { if (m.type === 'attributes') cleanNode(m.target); else if (m.addedNodes && m.addedNodes.length) { m.addedNodes.forEach(n => { if (n.nodeType === 1) scanTree(n); }); } } }); function start() { scanTree(document.documentElement); mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start, { once: true }); } else { start(); } // Shadow DOM 支持:覆盖 attachShadow,在 shadow root 内注入相同逻辑 const _attach = Element.prototype.attachShadow; if (_attach) { Element.prototype.attachShadow = function (init) { const root = _attach.call(this, init); try { const st = document.createElement('style'); st.textContent = css; root.appendChild(st); for (const t of BLOCKED) root.addEventListener(t, stopper, true); const mo2 = new MutationObserver(muts => { for (const m of muts) { if (m.type === 'attributes') cleanNode(m.target); else if (m.addedNodes && m.addedNodes.length) { m.addedNodes.forEach(n => { if (n.nodeType === 1) scanTree(n); }); } } }); mo2.observe(root, { subtree: true, childList: true, attributes: true }); } catch {} return root; }; } // 兜底:周期性把 html/body 拉回可选,防止站点反复写死 setInterval(() => { [document.documentElement, document.body].forEach(n => n && cleanNode(n)); }, 1500); })();