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

Protects clipboard access with permission prompts and enhanced UI

// ==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('剪贴板守护初始化完成。');
})();