剪贴板守护 (v30.0 终极版)

Protects clipboard access with permission prompts and enhanced UI

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         剪贴板守护 (v30.0 终极版)
// @name:en      Clipboard Guard (v30.0 Ultimate Edition)
// @namespace    https://tampermonkey.net/
// @version      30.0
// @description  Protects clipboard access with permission prompts and enhanced UI
// @author       WillArixq
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @grant        GM_listValues
// @grant        GM_deleteValue
// @run-at       document-start
// @license      MIT
// ==/UserScript==

/*
MIT License
Copyright (c) 2025 WillArixq
Licensed under the MIT License: https://opensource.org/licenses/MIT
Contact: X (https://x.com/sturverse9731), Email ([email protected])
*/
(async function() {
    'use strict';

    // --- [1] 全局配置 (增强) ---
    const CONFIG = {
        DEBUG_MODE: true,
        DEBOUNCE_MS: 150,
        PERMISSION_KEY_PREFIX: 'cb_perm_v12_',
        DIALOG_MAX_WIDTH: '90vw',
        TOAST_DURATION: 2500,
        THEME_KEY: 'cb_theme_v12',
        ACTIVE_KEY: 'cb_active_v12',
        STATUS_INDICATOR: true,
        AUTO_DENY_TIMEOUT: 18000 // 18 seconds
    };

    // --- [2] 日志系统 ---
    const Logger = {
        levels: { DEBUG: 1, INFO: 2, ERROR: 3 },
        currentLevel: CONFIG.DEBUG_MODE ? 1 : 2,
        log(level, ...args) {
            if (this.levels[level] >= this.currentLevel) {
                console[level.toLowerCase()](
                    `%c[守护 v30.0]`,
                    'background: #3949AB; color: #fff; padding: 2px 4px; border-radius: 4px;',
                    ...args
                );
            }
        },
        debug(...args) { this.log('DEBUG', ...args); },
        info(...args) { this.log('INFO', ...args); },
        error(...args) { this.log('ERROR', ...args); }
    };

    // --- [3] 国际化 ---
    const I18N = {
        zh: {
            title_read: '剪贴板"读取"请求',
            title_write: '剪贴板"写入"请求',
            batch_title: '批量剪贴板请求',
            deny_once: '拒绝',
            allow_once: '允许',
            deny_always: '永久禁止',
            allow_always: '永久允许',
            deny_all: '全部禁止',
            allow_all: '全部允许',
            source: '请求来源: <b>{hostname}</b>',
            preview: '预览 ({count} 字符)',
            batch_summary: '该网站在短时间内发起了 {write} 次写入{read}请求。',
            batch_preview: '以下为第一个写入请求的预览:',
            reset_permissions: '重置所有剪贴板权限',
            permission_saved: '权限设置已保存!',
            navigation_blocked: '请先处理剪贴板权限请求',
            toggle_on: '激活脚本',
            toggle_off: '禁用脚本',
            script_enabled: '剪贴板守护已激活',
            script_disabled: '剪贴板守护已禁用',
            theme_light: '切换至浅色主题',
            theme_dark: '切换至深色主题',
            theme_auto: '切换至自动主题'
        },
        en: {
            title_read: 'Clipboard "Read" Request',
            title_write: 'Clipboard "Write" Request',
            batch_title: 'Batch Clipboard Requests',
            deny_once: 'Deny',
            allow_once: 'Allow',
            deny_always: 'Permanently Deny',
            allow_always: 'Permanently Allow',
            deny_all: 'Deny All',
            allow_all: 'Allow All',
            source: 'Request from: <b>{hostname}</b>',
            preview: 'Preview ({count} characters)',
            batch_summary: 'This site initiated {write} write{read} requests in a short time.',
            batch_preview: 'Below is a preview of the first write request:',
            reset_permissions: 'Reset All Clipboard Permissions',
            permission_saved: 'Permission settings saved!',
            navigation_blocked: 'Please handle the clipboard permission request first',
            toggle_on: 'Enable Script',
            toggle_off: 'Disable Script',
            script_enabled: 'Clipboard guard is now enabled',
            script_disabled: 'Clipboard guard is now disabled',
            theme_light: 'Switch to Light Theme',
            theme_dark: 'Switch to Dark Theme',
            theme_auto: 'Switch to Auto Theme'
        }
    };

    const getLang = () => navigator.language.startsWith('zh') ? 'zh' : 'en';

    // --- [4] 图标 (增强) ---
    const ICONS = {
        shield: `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>`,
        globe: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>`,
        copy: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`,
        checkmark: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
        info: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="8"></line></svg>`,
        sun: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>`,
        moon: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`,
        desktop: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>`
    };

    // --- [5] CSS 样式(模块化与极致美化) ---
    const CSS_VARIABLES = `
        :root {
            --cb-transition-speed: 0.4s;
            --cb-bg: rgba(255, 255, 255, 0.95);
            --cb-text: #1d1d1f;
            --cb-text-light: #6e6e73;
            --cb-title: #000;
            --cb-pre-bg: rgba(120, 120, 128, 0.08);
            --cb-btn-secondary-bg: rgba(120, 120, 128, 0.15);
            --cb-btn-secondary-bg-hover: rgba(120, 120, 128, 0.25);
            --cb-btn-primary-bg: linear-gradient(145deg, #007FFF, #006AE0);
            --cb-btn-primary-bg-hover: linear-gradient(145deg, #0088FF, #0070E0);
            --cb-btn-primary-text: #fff;
            --cb-btn-secondary-text: #007aff;
            --cb-btn-deny-text-hover: #ff3b30;
            --cb-overlay-bg: rgba(0, 0, 0, 0.4);
            --cb-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.35), 0 0 1px rgba(0,0,0,0.1);
            --cb-border: 1px solid rgba(255, 255, 255, 0.6);
            --cb-icon-color: #007aff;
            --cb-toast-bg: rgba(0, 0, 0, 0.75);
            --cb-toast-text: #fff;
            --cb-status-indicator-active: #28a745;
            --cb-status-indicator-inactive: #6c757d;
            --cb-handle-bg: rgba(120, 120, 128, 0.2);
        }
        .cb-sentinel-dark-theme {
            --cb-bg: rgba(28, 28, 30, 0.92);
            --cb-text: #f2f2f7;
            --cb-text-light: #8e8e93;
            --cb-title: #fff;
            --cb-pre-bg: rgba(120, 120, 128, 0.2);
            --cb-btn-secondary-bg: rgba(120, 120, 128, 0.25);
            --cb-btn-secondary-bg-hover: rgba(120, 120, 128, 0.35);
            --cb-btn-primary-bg: linear-gradient(145deg, #0A84FF, #0063C7);
            --cb-btn-primary-bg-hover: linear-gradient(145deg, #0B97FF, #0A73E0);
            --cb-btn-secondary-text: #0a84ff;
            --cb-btn-deny-text-hover: #ff453a;
            --cb-overlay-bg: rgba(0, 0, 0, 0.5);
            --cb-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.65), 0 0 1px rgba(255,255,255,0.1);
            --cb-border: 1px solid rgba(255, 255, 255, 0.1);
            --cb-icon-color: #0a84ff;
            --cb-toast-bg: rgba(0, 0, 0, 0.85);
            --cb-toast-text: #f2f2f7;
            --cb-handle-bg: rgba(120, 120, 128, 0.3);
        }
    `;

    const CSS_ANIMATIONS = `
        @keyframes enter { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
        @keyframes exit { from { opacity: 1; transform: scale(1) translateY(0); } to { opacity: 0; transform: scale(0.95) translateY(10px); } }
        @keyframes wiggle { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
        @keyframes toast-enter { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
        @keyframes toast-exit { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(20px); } }
        @keyframes status-pulse {
            0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.4); }
            70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
            100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
        }
        .is-wiggling { animation: wiggle 0.3s ease-in-out; }
        .is-pulsing { animation: status-pulse 1.5s infinite; }
    `;

    const CSS_LAYOUT = `
        .cb-sentinel-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background-color: var(--cb-overlay-bg); backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px); z-index: 2147483647;
            display: flex; justify-content: center; align-items: center;
            opacity: 0; animation: enter var(--cb-transition-speed) cubic-bezier(0.16, 1, 0.3, 1) forwards;
            transition: all var(--cb-transition-speed) ease;
        }
        .cb-sentinel-overlay.is-closing { animation: exit calc(var(--cb-transition-speed) * 0.75) cubic-bezier(0.7, 0, 0.84, 0) forwards; }
        .cb-sentinel-dialog {
            background-color: var(--cb-bg); color: var(--cb-text); padding: 24px;
            border-radius: 22px; box-shadow: var(--cb-shadow); max-width: min(400px, ${CONFIG.DIALOG_MAX_WIDTH});
            border: var(--cb-border); display: flex; flex-direction: column; gap: 16px;
            font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            transition: all var(--cb-transition-speed) ease;
            opacity: 0; animation: enter var(--cb-transition-speed) cubic-bezier(0.16, 1, 0.3, 1) 0.05s forwards;
            position: relative;
        }
        .cb-sentinel-overlay.is-closing .cb-sentinel-dialog { animation: exit calc(var(--cb-transition-speed) * 0.75) cubic-bezier(0.7, 0, 0.84, 0) forwards; }
        .cb-dialog-handle {
            position: absolute; top: 0; left: 0; right: 0;
            height: 30px; cursor: move; border-radius: 22px 22px 0 0;
            display: flex; align-items: center; justify-content: center;
            opacity: 0; transition: opacity 0.2s ease;
        }
        .cb-dialog-handle:hover { opacity: 1; background-color: var(--cb-handle-bg); }
        .cb-header, .cb-origin { display: flex; align-items: center; justify-content: center; gap: 10px; text-align: center; }
        .cb-header .icon, .cb-origin .icon { display: inline-flex; align-items: center; justify-content: center; }
        .cb-header .icon { color: var(--cb-icon-color); transition: color var(--cb-transition-speed) ease; }
        .cb-header h3 { margin: 0; color: var(--cb-title); font-size: 19px; font-weight: 600; transition: color var(--cb-transition-speed) ease; }
        .cb-origin { color: var(--cb-text-light); font-size: 13.5px; line-height: 1.4; background: var(--cb-pre-bg); padding: 8px 14px; border-radius: 12px; transition: all var(--cb-transition-speed) ease; }
        .cb-origin b { font-weight: 500; color: var(--cb-text); }
        .cb-content-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: -8px; padding: 0 4px; }
        .cb-content-header span { font-size: 12px; color: var(--cb-text-light); }
        .cb-copy-button { background: none; border: none; cursor: pointer; color: var(--cb-text-light); padding: 4px; border-radius: 4px; transition: all 0.2s ease; }
        .cb-copy-button:hover { background: var(--cb-btn-secondary-bg); color: var(--cb-icon-color); }
        .cb-sentinel-dialog pre {
            background-color: var(--cb-pre-bg); border: none; padding: 12px; color: var(--cb-text);
            border-radius: 10px; max-height: 140px; overflow-y: auto; text-align: left;
            white-space: pre-wrap; word-break: break-all; font-size: 13px; font-family: "SF Mono", "Menlo", monospace;
            transition: all var(--cb-transition-speed) ease;
        }
        .cb-sentinel-buttons { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
        .cb-button-group { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
        .cb-button-separator { height: 1px; background-color: rgba(120,120,128,0.16); margin: 4px 0; }
        .cb-sentinel-buttons button {
            padding: 13px 0; border-radius: 12px; border: none; cursor: pointer;
            font-size: 16px; font-weight: 500; transition: all 0.2s ease;
            box-shadow: 0 1px 2px rgba(0,0,0,0.05);
        }
        .cb-sentinel-buttons button:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
        .cb-sentinel-buttons button:active { transform: scale(0.97); filter: brightness(0.95); box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); }
        .cb-sentinel-buttons button:focus { outline: 2px solid var(--cb-icon-color); outline-offset: 2px; }
        .cb-sentinel-buttons .secondary { background-color: var(--cb-btn-secondary-bg); color: var(--cb-btn-secondary-text); }
        .cb-sentinel-buttons .secondary:hover { background-color: var(--cb-btn-secondary-bg-hover); }
        .cb-sentinel-buttons .primary { background-image: var(--cb-btn-primary-bg); color: var(--cb-btn-primary-text); font-weight: 600; }
        .cb-sentinel-buttons .primary:hover { background-image: var(--cb-btn-primary-bg-hover); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,123,255,0.3); }
        .cb-sentinel-buttons .deny_always:hover { color: var(--cb-btn-deny-text-hover); }
        .cb-toast {
            position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
            background: var(--cb-toast-bg); color: var(--cb-toast-text); padding: 10px 18px;
            border-radius: 10px; font-size: 14px; z-index: 2147483648;
            opacity: 0; animation: toast-enter 0.3s ease forwards;
        }
        .cb-toast.is-closing { animation: toast-exit 0.3s ease forwards; }
        .cb-status-indicator {
            position: fixed; bottom: 15px; right: 15px; width: 36px; height: 36px;
            background-color: var(--cb-bg); border-radius: 50%; z-index: 2147483646;
            display: flex; justify-content: center; align-items: center;
            box-shadow: var(--cb-shadow); border: var(--cb-border);
            cursor: pointer; transition: all 0.2s ease;
        }
        .cb-status-indicator:hover { transform: translateY(-2px); box-shadow: var(--cb-shadow); }
        .cb-status-indicator .icon {
            color: var(--cb-status-indicator-inactive);
            transition: color 0.2s ease;
        }
        .cb-status-indicator.is-active .icon {
            color: var(--cb-status-indicator-active);
        }
    `;

    // 异步加载 CSS
    const loadCSS = () => GM_addStyle(`${CSS_VARIABLES}${CSS_ANIMATIONS}${CSS_LAYOUT}`);
    requestAnimationFrame(loadCSS);

    // --- [6] 工具函数 ---
    const escape = (str) => {
        if (!str) return '';
        const div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    };

    const showToast = (message) => {
        const existingToast = document.querySelector('.cb-toast');
        if (existingToast) existingToast.remove();
        const toast = document.createElement('div');
        toast.className = 'cb-toast';
        toast.textContent = message;
        document.body.appendChild(toast);
        setTimeout(() => {
            toast.classList.add('is-closing');
            toast.addEventListener('animationend', () => toast.remove(), { once: true });
        }, CONFIG.TOAST_DURATION);
    };

    // --- [7] 导航锁模块 ---
    const NavigationLocker = {
        isLocked: false,
        activate() { this.isLocked = true; },
        deactivate() { this.isLocked = false; }
    };

    const handleWindowEvents = (e) => {
        if (!NavigationLocker.isLocked || e.target.closest('.cb-sentinel-overlay')) return;

        if (e.type === 'beforeunload') {
            e.preventDefault();
            e.returnValue = I18N[getLang()].navigation_blocked;
            return e.returnValue;
        }

        if (e.cancelable) {
            e.preventDefault();
            e.stopPropagation();
            const dialog = activeDialog?.querySelector('.cb-sentinel-dialog');
            if (dialog) {
                dialog.classList.add('is-wiggling');
                dialog.addEventListener('animationend', () => dialog.classList.remove('is-wiggling'), { once: true });
                showToast(I18N[getLang()].navigation_blocked);
            }
        }
    };

    // --- [8] 对话框管理器 ---
    const DialogManager = {
        close() {
            if (activeDialog) {
                const overlay = activeDialog;
                overlay.classList.add('is-closing');
                overlay.addEventListener('animationend', () => {
                    overlay.remove();
                    if (activeDialog === overlay) activeDialog = null;
                }, { once: true });
                NavigationLocker.deactivate();
            }
        },

        create(options) {
            this.close();
            NavigationLocker.activate();
            return new Promise(resolve => {
                const overlay = document.createElement('div');
                overlay.className = 'cb-sentinel-overlay';
                activeDialog = overlay;

                const dialog = document.createElement('div');
                dialog.className = 'cb-sentinel-dialog';
                dialog.innerHTML = options.html;

                const handleButtonClick = (e) => {
                    const button = e.target.closest('button[data-value]');
                    if (button) {
                        resolve(button.dataset.value);
                        this.close();
                        return;
                    }
                };

                const handleCopyClick = (e) => {
                    const copyButton = e.target.closest('.cb-copy-button');
                    if (copyButton) {
                        const contentNode = dialog.querySelector('pre');
                        if (contentNode) {
                            GM_setClipboard(contentNode.textContent, 'text/plain');
                            copyButton.innerHTML = ICONS.checkmark;
                            copyButton.disabled = true;
                            setTimeout(() => {
                                copyButton.innerHTML = ICONS.copy;
                                copyButton.disabled = false;
                            }, 1500);
                        }
                    }
                };

                const handleKeydown = (e) => {
                    if (e.key === 'Enter') {
                        const denyButton = dialog.querySelector('.deny_once');
                        if (denyButton) {
                            resolve(denyButton.dataset.value);
                            this.close();
                        }
                    } else if (e.key === 'Escape') {
                        resolve('deny_once');
                        this.close();
                    }
                };

                dialog.addEventListener('click', handleButtonClick);
                dialog.addEventListener('click', handleCopyClick);
                dialog.addEventListener('keydown', handleKeydown);
                dialog.tabIndex = 0;
                overlay.appendChild(dialog);
                (document.body || document.documentElement).appendChild(overlay);
                dialog.focus();

                const applyTheme = (theme) => {
                    if (theme === 'dark') {
                        overlay.classList.add('cb-sentinel-dark-theme');
                    } else if (theme === 'light') {
                        overlay.classList.remove('cb-sentinel-dark-theme');
                    } else {
                        const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)');
                        overlay.classList.toggle('cb-sentinel-dark-theme', darkThemeMq.matches);
                    }
                };
                GM_getValue(CONFIG.THEME_KEY, 'auto').then(applyTheme);
            });
        }
    };

    // --- [9] 核心逻辑 ---
    let activeDialog = null;
    let requestQueue = [];

    const buildContentPreviewHTML = (content, lang) => {
        if (!content) return '';
        const charCount = content.length;
        return `<div class="cb-content-header"><span>${lang.preview.replace('{count}', charCount)}</span><button class="cb-copy-button" title="${lang.preview}">${ICONS.copy}</button></div><pre>${escape(content)}</pre>`;
    };

    const buildDialogHeaderHTML = (title, hostname, lang) => `
        <div class="cb-dialog-handle"></div>
        <div class="cb-header"><span class="icon">${ICONS.shield}</span><h3>${title}</h3></div>
        <div class="cb-origin"><span class="icon">${ICONS.globe}</span><span>${lang.source.replace('{hostname}', hostname)}</span></div>
    `;

    const buildSingleRequestDialogHTML = (hostname, type, content) => {
        const lang = I18N[getLang()];
        const title = lang[`title_${type}`];
        const contentHTML = type === 'write' ? buildContentPreviewHTML(content, lang) : '';
        return `
            ${buildDialogHeaderHTML(title, hostname, lang)}
            ${contentHTML}
            <div class="cb-sentinel-buttons">
                <button class="primary deny_once" data-value="deny_once" title="${lang.deny_once}">${lang.deny_once}</button>
                <div class="cb-button-separator"></div>
                <button class="secondary deny_always" data-value="deny_always" title="${lang.deny_always}">${lang.deny_always}</button>
            </div>`;
    };

    const buildBatchRequestDialogHTML = (hostname, requests) => {
        const lang = I18N[getLang()];
        const writeCount = requests.filter(r => r.type === 'write').length;
        const readCount = requests.filter(r => r.type === 'read').length;
        let summary = lang.batch_summary.replace('{write}', writeCount > 0 ? ` <b>${writeCount}</b>` : '');
        summary = summary.replace('{read}', readCount > 0 ? `${writeCount > 0 ? ' and ' : ''} <b>${readCount}</b>` : '');

        const writeRequests = requests.filter(r => r.type === 'write');
        let previewsHTML = '';
        if (writeRequests.length > 0) {
            previewsHTML = `
                <p style="font-size:12px;text-align:center;color:var(--cb-text-light);margin-top:-10px;">${lang.batch_preview}</p>
                ${writeRequests.map(req => buildContentPreviewHTML(req.content, lang)).join('')}
            `;
        }

        return `
            ${buildDialogHeaderHTML(lang.batch_title, hostname, lang)}
            <p style="text-align:center; padding: 10px 0; color:var(--cb-text-light);">${summary}</p>
            ${previewsHTML}
            <div class="cb-sentinel-buttons" style="grid-template-columns: 1fr; display: grid;">
                <button class="primary" data-value="allow_all" title="${I18N[getLang()].allow_all}">${I18N[getLang()].allow_all}</button>
                <button class="secondary deny_all" data-value="deny_all" title="${I18N[getLang()].deny_all}">${I18N[getLang()].deny_all}</button>
            </div>`;
    };

    const processRequestQueue = async () => {
        if (requestQueue.length === 0 || activeDialog) return;

        const isEnabled = await GM_getValue(CONFIG.ACTIVE_KEY, true);

        if (!isEnabled) {
            Logger.info('脚本已禁用,请求直接通过。');
            requestQueue.forEach(req => req.executor().then(req.resolve).catch(req.reject));
            requestQueue = [];
            return;
        }

        try {
            const hostname = unsafeWindow.location.hostname;
            const requestsToProcess = [...requestQueue];
            requestQueue = [];

            const firstRequest = requestsToProcess[0];
            const permissionKey = `${CONFIG.PERMISSION_KEY_PREFIX}${hostname}_${firstRequest.type}`;
            const storedPermission = await GM_getValue(permissionKey);

            if (storedPermission === 'deny') {
                Logger.info('检测到永久禁止权限,自动拒绝请求。');
                requestsToProcess.forEach(req => req.reject(new DOMException('Clipboard access permanently denied by user.', 'NotAllowedError')));
                return;
            }
            if (storedPermission === 'allow') {
                Logger.info('检测到永久允许权限,自动允许请求。');
                requestsToProcess.forEach(req => req.executor().then(req.resolve).catch(req.reject));
                return;
            }

            const html = requestsToProcess.length === 1
                ? buildSingleRequestDialogHTML(hostname, firstRequest.type, firstRequest.content)
                : buildBatchRequestDialogHTML(hostname, requestsToProcess);

            const userChoice = await DialogManager.create({ html });

            let shouldDeny = false;

            switch (userChoice) {
                case 'deny_always':
                    await GM_setValue(permissionKey, 'deny');
                    shouldDeny = true;
                    showToast(I18N[getLang()].permission_saved);
                    break;
                case 'deny_once':
                case 'deny_all':
                    shouldDeny = true;
                    break;
                case 'allow_all':
                    await GM_setValue(permissionKey, 'allow');
                    shouldDeny = false;
                    showToast(I18N[getLang()].permission_saved);
                    break;
                default:
                    shouldDeny = true;
                    break;
            }

            if (shouldDeny) {
                requestsToProcess.forEach(req => req.reject(new DOMException('Clipboard access denied by user.', 'NotAllowedError')));
            } else {
                requestsToProcess.forEach(req => req.executor().then(req.resolve).catch(req.reject));
            }
        } catch (e) {
            Logger.error('处理请求队列时出错:', e);
            requestQueue.forEach(req => req.reject(new DOMException('Internal error in request queue.', 'AbortError')));
            requestQueue = [];
        }
    };

    const enqueueRequest = (requestDetails) => {
        return new Promise((resolve, reject) => {
            requestQueue.push({ ...requestDetails, resolve, reject });
            processRequestQueue();
        });
    };

    // --- [10] 拦截钩子模块 ---
    let originalClipboard = null;
    let originalExecCommand = null;

    const applyguardHooks = async () => {
        if (unsafeWindow.navigator.clipboard?.isguard) return;
        originalClipboard = originalClipboard || unsafeWindow.navigator.clipboard || {};
        originalExecCommand = originalExecCommand || unsafeWindow.document.execCommand;

        const clipboardProxy = new Proxy(originalClipboard, {
            get(target, prop) {
                if (!isScriptActive) {
                    return Reflect.get(target, prop);
                }

                if (['readText', 'read', 'writeText', 'write'].includes(prop) && target[prop]) {
                    return async function(...args) {
                        const type = prop.includes('read') ? 'read' : 'write';
                        const content = (prop === 'writeText' && args[0]) || (prop === 'write' && args[0]?.toString()) || null;

                        if (prop === 'writeText' && GM_setClipboard) {
                            const executor = () => {
                                GM_setClipboard(content, 'text/plain');
                                return Promise.resolve(content);
                            };
                            return enqueueRequest({ type, content, executor });
                        }

                        const executor = () => Reflect.apply(target[prop], target, args);
                        return enqueueRequest({ type, content, executor });
                    };
                }
                if (prop === 'isguard') return true;
                return Reflect.get(target, prop);
            }
        });

        Object.defineProperty(unsafeWindow.navigator, 'clipboard', { value: clipboardProxy, writable: true, configurable: true });

        if (originalExecCommand) {
            if (unsafeWindow.document.execCommand?.isguard) return;
            const execCommandOverride = function(cmd, ...args) {
                const isEnabled = isScriptActive;

                if (!isEnabled) {
                    return Reflect.apply(originalExecCommand, unsafeWindow.document, [cmd, ...args]);
                }

                const command = cmd.toLowerCase();
                if (['copy', 'cut', 'paste'].includes(command)) {
                    const type = command === 'paste' ? 'read' : 'write';
                    const content = type === 'write' ? unsafeWindow.getSelection()?.toString() : null;
                    const executor = () => Promise.resolve(Reflect.apply(originalExecCommand, unsafeWindow.document, [cmd, ...args]));
                    return enqueueRequest({ type, content, executor });
                }
                return Reflect.apply(originalExecCommand, unsafeWindow.document, [cmd, ...args]);
            };
            execCommandOverride.isguard = true;
            Object.defineProperty(unsafeWindow.document, 'execCommand', { value: execCommandOverride, writable: true, configurable: true });
        }
    };

    const clipboardEventListener = async (e) => {
        if (!(await GM_getValue(CONFIG.ACTIVE_KEY, true))) return;

        e.preventDefault();
        const type = e.type === 'paste' ? 'read' : 'write';
        const content = type === 'write' ? unsafeWindow.getSelection()?.toString() : null;
        enqueueRequest({
            type,
            content,
            executor: () => {
                const event = new Event(e.type, { bubbles: true, cancelable: true });
                window.dispatchEvent(event);
                return Promise.resolve();
            }
        });
    };

    // --- [11] 状态指示器模块 ---
    const StatusIndicator = {
        element: null,
        async init() {
            if (!CONFIG.STATUS_INDICATOR || document.querySelector('.cb-status-indicator')) return;
            this.element = document.createElement('div');
            this.element.className = 'cb-status-indicator';
            this.element.innerHTML = `<span class="icon">${ICONS.shield}</span>`;
            this.element.addEventListener('click', () => {
                showToast('Clipboard guard');
            });
            document.body.appendChild(this.element);
            this.updateStatus();
        },
        async updateStatus() {
            if (!this.element) return;
            const isActive = await GM_getValue(CONFIG.ACTIVE_KEY, true);
            this.element.classList.toggle('is-active', isActive);
            this.element.title = isActive ? I18N[getLang()].script_enabled : I18N[getLang()].script_disabled;
        }
    };

    // --- [12] 初始化与监控 ---
    let isScriptActive;

    const toggleScript = async () => {
        isScriptActive = !isScriptActive;
        await GM_setValue(CONFIG.ACTIVE_KEY, isScriptActive);
        showToast(isScriptActive ? I18N[getLang()].script_enabled : I18N[getLang()].script_disabled);
        StatusIndicator.updateStatus();
        updateMenuCommands();
    };

    const updateMenuCommands = () => {
        GM_registerMenuCommand(I18N[getLang()].reset_permissions, async () => {
            const keys = await GM_listValues();
            for (const key of keys) {
                if (key.startsWith(CONFIG.PERMISSION_KEY_PREFIX)) {
                    await GM_deleteValue(key);
                }
            }
            showToast(I18N[getLang()].reset_permissions);
        });

        GM_registerMenuCommand(isScriptActive ? I18N[getLang()].toggle_off : I18N[getLang()].toggle_on, toggleScript);

        GM_registerMenuCommand(I18N[getLang()].theme_auto, async () => {
            await GM_setValue(CONFIG.THEME_KEY, 'auto');
            showToast(`Theme set to Auto.`);
        });
        GM_registerMenuCommand(I18N[getLang()].theme_light, async () => {
            await GM_setValue(CONFIG.THEME_KEY, 'light');
            showToast(`Theme set to Light.`);
        });
        GM_registerMenuCommand(I18N[getLang()].theme_dark, async () => {
            await GM_setValue(CONFIG.THEME_KEY, 'dark');
            showToast(`Theme set to Dark.`);
        });
    };

    // 注册核心事件监听器
    isScriptActive = await GM_getValue(CONFIG.ACTIVE_KEY, true);
    applyguardHooks();

    window.addEventListener('copy', clipboardEventListener, true);
    window.addEventListener('cut', clipboardEventListener, true);
    window.addEventListener('paste', clipboardEventListener, true);
    window.addEventListener('click', handleWindowEvents, { capture: true });
    window.addEventListener('submit', handleWindowEvents, { capture: true });
    window.addEventListener('beforeunload', handleWindowEvents, { capture: true });
    window.addEventListener('popstate', handleWindowEvents, { capture: true });

    // 监听DOM变化,确保钩子保持激活
    if (document.body) {
        const observer = new MutationObserver(() => {
            if (!unsafeWindow.navigator.clipboard?.isguard || !unsafeWindow.document.execCommand?.isguard) {
                Logger.debug('检测到DOM变化,重新应用钩子。');
                applyguardHooks();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        window.addEventListener('unload', () => observer.disconnect());
    }

    // 注册菜单命令
    updateMenuCommands();

    // 初始化状态指示器
    if (CONFIG.STATUS_INDICATOR) {
        document.addEventListener('DOMContentLoaded', () => {
            StatusIndicator.init();
        });
    }

    Logger.info('剪贴板守护初始化完成。');
})();