您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
控制网站的写入剪贴板操作,提供允许/拒绝选项
// ==UserScript== // @name 剪贴板权限控制 // @description 控制网站的写入剪贴板操作,提供允许/拒绝选项 // @version 1.0 // @author WJ // @match *://*/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_registerMenuCommand // @run-at document-start // @namespace https://greasyfork.org/users/914996 // ==/UserScript== // 脚本默认不拦截主动复制! (() => { 'use strict'; /* ---------- 数据 ---------- */ let whitelist = GM_getValue('whitelist', []); let blacklist = GM_getValue('blacklist', []); const domain = location.hostname; const save = () => { GM_setValue('whitelist', whitelist); GM_setValue('blacklist', blacklist); }; /* ---------- 工具 ---------- */ const toast = (msg) => { const t = document.createElement('div'); t.className = 'WJ_toast'; t.textContent = msg; document.body.append(t); setTimeout(() => t.remove(), 3000); }; /* 1. Clipboard API writeText */ navigator.clipboard.writeText = (text) => new Promise((res) => { decide(text, () => { GM_setClipboard(text); res(); }, res, '[writeText]'); }); /* 2. copy 事件 */ const rawAdd = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function(t, l, o) { if (t === 'copy' && this === document) return; if (!o && ['touchstart','touchmove','wheel'].includes(t)) o = { passive: true }; rawAdd.call(this, t, l, o); }; document.oncopy = (e) => { e?.preventDefault?.(); e?.stopImmediatePropagation?.(); const text = getSelection().toString(); decide(text, () => GM_setClipboard(text), null, '[copy]'); return true; }; /* 3. execCommand('copy cut') */ const originalExecCommand = Document.prototype.execCommand; Document.prototype.execCommand = function (command, showUI, value) { if (command.toLowerCase() === 'copy' || command.toLowerCase() === 'cut') { const text = getSelection().toString(); decide(text, () => GM_setClipboard(text), null, '[exec]'); return true; } return originalExecCommand.call(this, command, showUI, value); }; /* 4. Clipboard API write (富文本) */ const nativeWrite = navigator.clipboard.write; navigator.clipboard.write = d => new Promise((r, j) => { (d[0]?.types?.includes('text/plain') ? d[0].getType('text/plain').then(b => new Response(b).text()) : Promise.reject('No text/plain type')).then(t => decide(t, () => { GM_setClipboard(t); r(); }, () => j(new Error('User denied clipboard write')), '[write富文本]' )).catch(() => decide('富文本内容?图片/表格 无法显示', () => nativeWrite(d).then(r).catch(j), () => j(new Error('User denied clipboard write')), '[write富文本]' )); }); /* ---------- 弹窗 ---------- */ let isModalOpen = false; const decide = (text, onAllow, onDeny, via = '未知') => { if (isModalOpen) { if (!document.querySelector('.WJ_warning')) { const header = document.querySelector('.WJ_modal .WJ_header'); const warning = document.createElement('div'); warning.className = 'WJ_warning'; warning.textContent = '⚠️ 此网站授权期多次尝试写入剪贴板 已拒绝后续写入 ⚠️'; Object.assign(warning.style, { color: '#FFD700', fontSize: '16px', marginTop: '8px' }); header.appendChild(warning); } return onDeny?.(); } if (whitelist.includes(domain)) return onAllow?.(); if (blacklist.includes(domain)) return (toast('已拦截复制'), onDeny?.()); const selection = getSelection(); const hasText = !!selection?.toString().trim(); const visible = !!(selection?.rangeCount && selection.getRangeAt(0).getClientRects().length); console.log('选区是否有文本:', hasText); console.log('选区是否可见:', visible); if (visible && hasText) return onAllow?.(); isModalOpen = true; const overlay = Object.assign(document.createElement('div'), { className: 'WJ_overlay' }); const modal = Object.assign(document.createElement('div'), { className: 'WJ_modal' }); modal.innerHTML = ` <div class="WJ_header">${domain} 请求写入剪贴板 ${via}</div> <div class="WJ_text">${text}</div> <div class="WJ_footer"> <button class="WJ_btn WJ_allow" data-action="allow">允许一次</button> <button class="WJ_btn WJ_deny" data-action="deny">拒绝一次</button> <button class="WJ_btn WJ_always-allow" data-action="always-allow">始终允许</button> <button class="WJ_btn WJ_always-deny" data-action="always-deny">始终拒绝</button> </div>`; document.body.append(overlay, modal); const close = () => (overlay.remove(), modal.remove(), isModalOpen = false); const actions = { allow: () => (toast('允许本次复制'), onAllow?.(), close()), deny: () => (toast('拒绝本次复制'), onDeny?.(), close()), 'always-allow': () => (whitelist.push(domain), save(), toast(`添加白名单 ${domain}`), onAllow?.(), close()), 'always-deny': () => (blacklist.push(domain), save(), toast(`添加黑名单 ${domain}`), onDeny?.(), close()) }; modal.onclick = e => { const action = e.target.dataset.action; if (actions[action]) actions[action](); }; overlay.onclick = () => actions.deny(); }; /* ---------- 管理面板 ---------- */ const showPanel = () => { const overlay = Object.assign(document.createElement('div'), { className: 'WJ_overlay' }); const panel = Object.assign(document.createElement('div'), { className: 'WJ_modal' }); panel.innerHTML = ` <div class="WJ_header">黑白名单-管理面板</div> <div class="WJ_panel-content"> <div class="WJ_list"> <div class="WJ_list-title">白名单</div> ${whitelist.map(d => ` <div class="WJ_list-item"> <span>${d}</span> <button class="WJ_delete" data-list="whitelist" data-domain="${d}">删除</button> </div>`).join('') || '<div class="WJ_empty">白名单为空</div>'} </div> <div class="WJ_list"> <div class="WJ_list-title">黑名单</div> ${blacklist.map(d => ` <div class="WJ_list-item"> <span>${d}</span> <button class="WJ_delete" data-list="blacklist" data-domain="${d}">删除</button> </div>`).join('') || '<div class="WJ_empty">黑名单为空</div>'} </div> </div> <div class="WJ_close-box"> <button class="WJ_btn WJ_close" id="WJ_close">关闭面板</button> </div>`; document.body.append(overlay, panel); panel.addEventListener('click', e => { if (e.target.id === 'WJ_close') return overlay.remove(), panel.remove(); if (e.target.classList.contains('WJ_delete')) { const { list, domain } = e.target.dataset; list === 'whitelist' ? whitelist = whitelist.filter(d => d !== domain) : blacklist = blacklist.filter(d => d !== domain); save(); overlay.remove(); panel.remove(); showPanel(); } }); }; /* ---------- 样式 ---------- */ GM_addStyle(` .WJ_modal,.WJ_overlay+div{position:fixed;top:65%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:560px;background:#121212;z-index:99999;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.3);font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;overflow:hidden;} .WJ_header{background:linear-gradient(135deg,#4a6fa5,#3a5a8a);color:#fff;padding:20px;font-size:20px;font-weight:600;text-align:center;letter-spacing:.5px;} .WJ_text{white-space:pre-wrap;word-break:break-word;background:#1F2021;padding:5px;border-radius:8px;height:220px;overflow-y:auto;font-family:Consolas,monospace;font-size:15px;line-height:1.5;color:#ccc;margin:15px;border:1px solid #333;} .WJ_footer{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:15px;background:#1F1F1F;} .WJ_btn{padding:12px 5px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-size:14px;color:#fff;text-align:center;transition:transform .1s,filter .1s;} .WJ_btn:active{transform:scale(0.98);} .WJ_allow{background:linear-gradient(135deg,#4CAF50,#2E7D32);} .WJ_deny{background:linear-gradient(135deg,#FF9800,#EF6C00);} .WJ_always-allow{background:linear-gradient(135deg,#2196F3,#1565C0);} .WJ_always-deny{background:linear-gradient(135deg,#F44336,#C62828);} .WJ_close{background:#3D5E90;padding:14px 40px;margin:0 auto;} .WJ_toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.9);color:#fff;padding:16px 32px;border-radius:8px;border:2px solid #CCC;z-index:99999;font-size:16px;font-weight:500;} .WJ_overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:99998;backdrop-filter:blur(8px);} .WJ_panel-content{display:flex;padding:0;max-height:60vh;} .WJ_list{flex:1;padding:20px;overflow-y:auto;border-right:1px solid #333;background:#121212;} .WJ_list:last-child{border-right:none;} .WJ_list-title{font-weight:600;margin:0 0 15px;padding-bottom:10px;border-bottom:2px solid #4a6fa5;color:#AAA;text-align:center;font-size:18px;} .WJ_list-item{display:flex;justify-content:space-between;align-items:center;padding:12px 18px;background:#1F2021;margin-bottom:10px;border-radius:8px;border:1px solid #333;} .WJ_list-item span{overflow:hidden;text-overflow:ellipsis;color:#ddd;} .WJ_delete{background:linear-gradient(135deg,#dc3545,#c82333);color:#fff;border:none;border-radius:6px;padding:6px 14px;cursor:pointer;font-size:14px;font-weight:500;margin-left:10px;} .WJ_close-box{padding:20px;text-align:center;border-top:1px solid #333;background:#1F2021;} .WJ_empty{text-align:center;padding:20px;color:#6c757d;font-style:italic;}`); GM_registerMenuCommand('黑白名单管理', showPanel); })();