Greasy Fork

Greasy Fork is available in English.

诡镇奇谈 - 可互动提示

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         诡镇奇谈 - 可互动提示 
// @namespace    http://tampermonkey.net/
// @license MIT
// @version      5.6
// @description  无
// @author       You
// @match        *://*/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置项 ---
    const CONFIG = {
        interactClassRegex: /^(?:([a-zA-Z0-9_-]+)--)?can-interact$/,
        checkEveryNFrames: 2
    };

    // --- 全局状态 & 本地存储 ---
    let $panel, $content;
    let frameCount = 0;
    let lastFingerprint = "";
    let lastAutoClickTime = 0;
    let isMinimized = false;

    // 熔断保护与暂停状态
    let isPaused = false;
    let clickHistory = []; // 记录最近的点击时间戳

    // 记录用户打钩的卡牌 ID (刷新失效)
    const autoSkipCards = new Map();
    // 防止重复请求API的锁
    const fetchingIds = new Set();

    // --- 数据持久化 (LocalStorage) ---
    const LOCAL_NAMES_KEY = 'tm-arkham-card-names';
    const SETTINGS_KEY = 'tm-arkham-settings';

    let cardNames = {};
    let settings = {
        myInvestigator: '',
        neverSkipMyTurn: true
    };

    try { cardNames = JSON.parse(localStorage.getItem(LOCAL_NAMES_KEY) || '{}'); } catch(e) {}
    try { settings = Object.assign(settings, JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}')); } catch(e) {}

    /**
     * 调用 ArkhamDB API 获取卡牌中文名
     */
    async function fetchCardNameFromAPI(baseId) {
        if (!baseId || fetchingIds.has(baseId) || cardNames[baseId]) return;

        fetchingIds.add(baseId);

        // 核心修改 1:处理带有后缀的特殊ID (例如 03006_Rogue_Mutated20)
        // 尝试提取最前面的 5 位数字作为真实的 ArkhamDB 卡牌编号
        let apiId = baseId;
        const prefixMatch = baseId.match(/^(\d{5})/);
        if (prefixMatch) {
            apiId = prefixMatch[1];
        } else {
            apiId = baseId.replace(/[a-zA-Z]$/, '');
        }

        try {
            const response = await fetch(`https://zh.arkhamdb.com/api/public/card/${apiId}`);
            if (response.ok) {
                const data = await response.json();
                if (data && data.name) {
                    cardNames[baseId] = data.name;
                    localStorage.setItem(LOCAL_NAMES_KEY, JSON.stringify(cardNames));
                    lastFingerprint = "";
                    return;
                }
            }
            throw new Error("无效返回或未找到卡牌");
        } catch (error) {
            cardNames[baseId] = baseId;
            localStorage.setItem(LOCAL_NAMES_KEY, JSON.stringify(cardNames));
        } finally {
            fetchingIds.delete(baseId);
        }
    }

    /**
     * 切换暂停状态
     */
    function setPauseState(paused) {
        isPaused = paused;
        const $pauseBtn = $panel.querySelector('.tm-pause-btn');
        if ($pauseBtn) {
            if (paused) {
                $pauseBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fca5a5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`;
                $pauseBtn.title = "已暂停 (点击恢复)";
            } else {
                $pauseBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4ade80" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>`;
                $pauseBtn.title = "点击暂停自动跳过";
            }
        }
        lastFingerprint = ""; // 强制刷新界面以显示或隐藏警告条
    }

    /**
     * 注入专属CSS
     */
    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            #tm-interact-panel { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s; }
            #tm-interact-panel.tm-minimized {
                left: auto !important; right: 0 !important;
                transform: translateX(calc(100% - 24px)) !important;
                width: auto !important; min-width: 0 !important; cursor: pointer;
            }
            #tm-interact-panel.tm-minimized .tm-header-normal,
            #tm-interact-panel.tm-minimized .tm-content { display: none !important; }
            #tm-interact-panel.tm-minimized .tm-restore-tab { display: flex !important; }
            .tm-restore-tab {
                display: none; padding: 12px 6px; align-items: center;
                justify-content: center; color: #fff; opacity: 0.8;
            }
            .tm-restore-tab:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); }
            .tm-skip-checkbox {
                appearance: none; width: 16px; height: 16px;
                background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.5);
                border-radius: 4px; cursor: pointer; position: relative; flex-shrink: 0;
            }
            .tm-skip-checkbox:checked { background: #4ade80; border-color: #4ade80; }
            .tm-skip-checkbox:checked::after {
                content: ''; position: absolute; left: 4px; top: 1px;
                width: 4px; height: 8px; border: solid white;
                border-width: 0 2px 2px 0; transform: rotate(45deg);
            }
            .tm-content { max-height: 50vh; overflow-y: auto; }
            .tm-content::-webkit-scrollbar { width: 4px; }
            .tm-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 2px; }
        `;
        document.head.appendChild(style);
    }

    /**
     * 初始化面板
     */
    function createPanel() {
        if ($panel) return;
        injectStyles();

        $panel = document.createElement('div');
        $panel.id = 'tm-interact-panel';
        $panel.style.cssText = `
            position: fixed; top: 20px; right: 20px; z-index: 999999;
            background: rgba(94, 123, 115, 0.9); border-radius: 16px;
            box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.3);
            backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
            overflow: hidden; width: clamp(220px, 25vw, 320px); max-width: fit-content;
            display: none; flex-direction: column; color: #ffffff;
            font-family: system-ui, sans-serif; font-size: 13px;
        `;

        const $headerNormal = document.createElement('div');
        $headerNormal.className = 'tm-header-normal';
        $headerNormal.style.cssText = `
            padding: 10px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            background: rgba(0, 0, 0, 0.25); display: flex;
            justify-content: space-between; align-items: center; cursor: grab; user-select: none;
        `;
        $headerNormal.innerHTML = `
            <span style="margin: 0; font-size: 14px; font-weight: bold; pointer-events: none;">可互动提示</span>
            <div style="display: flex; gap: 8px; align-items: center;">
                <button class="tm-pause-btn" style="background: none; border: none; color: inherit; cursor: pointer; padding: 2px; display: flex; opacity: 0.8;" title="点击暂停自动跳过">
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4ade80" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>
                </button>
                <button class="tm-min-btn" style="background: none; border: none; color: inherit; cursor: pointer; padding: 2px; display: flex; opacity: 0.8;" title="最小化面板">
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
                </button>
            </div>
        `;

        const $restoreTab = document.createElement('div');
        $restoreTab.className = 'tm-restore-tab';
        $restoreTab.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"></polyline></svg>`;

        $content = document.createElement('div');
        $content.className = 'tm-content';
        $content.style.cssText = `padding: 12px 16px; display: flex; flex-direction: column; gap: 8px;`;

        $panel.appendChild($headerNormal);
        $panel.appendChild($restoreTab);
        $panel.appendChild($content);
        document.body.appendChild($panel);

        // 按钮事件绑定
        const $pauseBtn = $headerNormal.querySelector('.tm-pause-btn');
        $pauseBtn.addEventListener('click', (e) => { e.stopPropagation(); setPauseState(!isPaused); });
        $pauseBtn.addEventListener('mouseenter', () => $pauseBtn.style.opacity = '1');
        $pauseBtn.addEventListener('mouseleave', () => $pauseBtn.style.opacity = '0.8');

        // 拖拽与最小化逻辑
        let isDragging = false;
        $headerNormal.addEventListener('mousedown', (e) => {
            if (e.target.closest('button')) return;
            isDragging = true; $headerNormal.style.cursor = 'grabbing';
            const rect = $panel.getBoundingClientRect();
            $panel.style.right = 'auto'; $panel.style.bottom = 'auto';
            $panel.style.left = rect.left + 'px'; $panel.style.top = rect.top + 'px';
            const startX = e.clientX, startY = e.clientY, startLeft = rect.left, startTop = rect.top;

            const onMouseMove = (moveEvent) => {
                if (!isDragging) return;
                $panel.style.left = (startLeft + moveEvent.clientX - startX) + 'px';
                $panel.style.top = (startTop + moveEvent.clientY - startY) + 'px';
            };
            const onMouseUp = () => {
                isDragging = false; $headerNormal.style.cursor = 'grab';
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            };
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });

        const $minBtn = $headerNormal.querySelector('.tm-min-btn');
        $minBtn.addEventListener('click', (e) => { e.stopPropagation(); isMinimized = true; $panel.classList.add('tm-minimized'); });
        $minBtn.addEventListener('mouseenter', () => $minBtn.style.opacity = '1');
        $minBtn.addEventListener('mouseleave', () => $minBtn.style.opacity = '0.8');
        $restoreTab.addEventListener('click', (e) => { e.stopPropagation(); isMinimized = false; $panel.classList.remove('tm-minimized'); });
    }

    /**
     * 抓取游戏局内的玩家信息和当前行动回合
     */
    function getPlayersInfo() {
        const playerTabs = document.querySelectorAll('ul.tabs__header li');
        let currentActivePlayer = '';
        const availablePlayers = [];

        playerTabs.forEach(li => {
            const span = li.querySelector('span');
            if (span) {
                const name = span.textContent.trim();
                availablePlayers.push(name);
                if (li.classList.contains('tab--active-player')) {
                    currentActivePlayer = name;
                }
                if (!settings.myInvestigator && li.classList.contains('tab--selected')) {
                    settings.myInvestigator = name;
                    localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
                }
            }
        });
        return { currentActivePlayer, availablePlayers };
    }

    function getCardDetails(el) {
        let baseId = "";
        const img = el.tagName.toLowerCase() === 'img' ? el : el.querySelector('img');
        if (img && img.src) {
            // 核心修改 2:兼容任意非 / 和 . 的文件名字符,以支持类似 03006_Rogue_Mutated20 的命名
            const match = img.src.match(/\/cards\/([^/.]+)\.[a-zA-Z0-9]+$/);
            if (match) baseId = match[1];
        }

        let uniqueId = "";
        const asset = el.closest('.asset') || el.closest('.asset--outer');
        const container = el.closest('.card-container');

        if (asset && asset.dataset.index) {
            uniqueId = asset.dataset.index;
        } else if (container && container.dataset.index) {
            uniqueId = container.dataset.index;
        } else {
            uniqueId = el.dataset.playabilityCardId || el.dataset.index ||
                       (img && img.dataset.playabilityCardId) || (img && img.dataset.id) ||
                       (img && img.dataset.index) || baseId ||
                       ('tm-' + Math.random().toString(36).substring(2));
        }

        let posLabel = "场上";
        const hand = document.querySelector('section.hand');
        if (hand) {
            const handContainer = el.closest('.card-container');
            if (handContainer && hand.contains(handContainer)) {
                const containers = Array.from(hand.querySelectorAll('.card-container'));
                const idx = containers.indexOf(handContainer);
                if (idx >= 0) posLabel = `手牌区第${idx + 1}张`;
            }
        }
        if (posLabel === "场上") {
            const inPlay = document.querySelector('section.in-play');
            if ((inPlay && inPlay.contains(el)) || el.closest('section.in-play')) {
                posLabel = "装备区";
            }
        }

        return { uniqueId, baseId, posLabel };
    }

    function createCheckboxItem(cardData, isChecked, isInactive = false) {
        const { baseId, posLabel } = cardData;
        let customName = baseId;
        if (baseId) {
            if (cardNames[baseId]) customName = cardNames[baseId];
            else if (fetchingIds.has(baseId)) customName = `${baseId} (获取中...)`;
        } else {
            customName = "未知";
        }

        const displayText = `${posLabel} - ${customName}`;
        const $label = document.createElement('label');
        $label.style.cssText = `display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 4px 0; opacity: ${isInactive ? '0.6' : '1'};`;

        const $text = document.createElement('span');
        $text.textContent = displayText;
        $text.title = "点击可持久化自定义卡牌名称";
        $text.style.cssText = 'white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 75%; border-bottom: 1px dashed rgba(255,255,255,0.4);';

        $text.addEventListener('click', (e) => {
            e.preventDefault(); e.stopPropagation();
            if (!baseId) { alert("未能提取到卡牌基础编号,无法命名。"); return; }
            const newName = prompt(`正在为卡牌 [${baseId}] 命名\n(输入留空则删除自定义):`, cardNames[baseId] || "");
            if (newName !== null) {
                if (newName.trim() === "") delete cardNames[baseId];
                else cardNames[baseId] = newName.trim();
                localStorage.setItem(LOCAL_NAMES_KEY, JSON.stringify(cardNames));
                lastFingerprint = "";
            }
        });

        const $checkbox = document.createElement('input');
        $checkbox.type = 'checkbox'; $checkbox.className = 'tm-skip-checkbox'; $checkbox.checked = isChecked;
        $checkbox.addEventListener('change', (e) => {
            if (e.target.checked) autoSkipCards.set(cardData.uniqueId, cardData);
            else autoSkipCards.delete(cardData.uniqueId);
            lastFingerprint = "";
        });

        $label.appendChild($text); $label.appendChild($checkbox);
        return $label;
    }

    /**
     * 统一的 UI 渲染函数
     */
    function renderUnifiedPanel(isSkipMode, currentCounts, activeSkipCards, playerInfo, preventSkip) {
        if (!$content) return;
        $content.innerHTML = '';
        let hasContent = false;

        // --- 0. 顶部回合与设置模块 ---
        if (playerInfo.availablePlayers.length > 0) {
            hasContent = true;
            const $turnHeader = document.createElement('div');
            $turnHeader.style.cssText = 'font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.2); margin-bottom: 6px; padding-bottom: 6px;';
            $turnHeader.innerHTML = `
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
                    <span style="color: #d1d5db;">行动阶段: <strong style="color: ${playerInfo.currentActivePlayer === settings.myInvestigator ? '#4ade80' : '#60a5fa'};">${playerInfo.currentActivePlayer || '未知'}</strong></span>
                </div>
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
                    <span style="color: #9ca3af;">本命角色:</span>
                    <select class="tm-my-player-select" style="background: rgba(0,0,0,0.4); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; padding: 2px; outline: none; font-size: 11px; max-width: 130px;">
                        <option value="">未指定</option>
                        ${playerInfo.availablePlayers.map(p => `<option value="${p}" ${settings.myInvestigator === p ? 'selected' : ''}>${p}</option>`).join('')}
                    </select>
                </div>
                <label style="display: flex; align-items: center; cursor: pointer; color: #9ca3af; font-size: 11px;">
                    <input type="checkbox" class="tm-no-skip-checkbox" ${settings.neverSkipMyTurn ? 'checked' : ''} style="margin-right: 6px;">
                    我的回合不自动跳过
                </label>
            `;

            const $select = $turnHeader.querySelector('.tm-my-player-select');
            $select.addEventListener('change', (e) => {
                settings.myInvestigator = e.target.value;
                localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
                lastFingerprint = "";
            });
            const $cb = $turnHeader.querySelector('.tm-no-skip-checkbox');
            $cb.addEventListener('change', (e) => {
                settings.neverSkipMyTurn = e.target.checked;
                localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
                lastFingerprint = "";
            });

            $content.appendChild($turnHeader);
        }

        // --- 1. 警告条展示 (暂停状态 / 回合保护) ---
        if (isPaused) {
            hasContent = true;
            const $pausedAlert = document.createElement('div');
            $pausedAlert.innerHTML = `<strong>⏸️ 自动跳过已暂停</strong><br/><span style="font-size: 10px; font-weight: normal;">(点击右上角 ▶️ 恢复)</span>`;
            $pausedAlert.style.cssText = 'color: #fca5a5; font-size: 12px; margin-bottom: 6px; padding: 6px 4px; background: rgba(239, 68, 68, 0.2); border-radius: 4px; text-align: center; line-height: 1.2;';
            $content.appendChild($pausedAlert);
        } else if (isSkipMode && preventSkip) {
            hasContent = true;
            const $warning = document.createElement('div');
            $warning.textContent = "当前是你的回合,自动跳过暂时挂起";
            $warning.style.cssText = 'color: #93c5fd; font-size: 11px; margin-bottom: 4px; padding: 4px; background: rgba(59, 130, 246, 0.2); border-radius: 4px; text-align: center;';
            $content.appendChild($warning);
        }

        // --- 2. 普通提示模式 ---
        if (!isSkipMode && Object.keys(currentCounts).length > 0) {
            hasContent = true;
            Object.keys(currentCounts).forEach(key => {
                const $item = document.createElement('div');
                $item.style.cssText = `display: flex; justify-content: space-between; align-items: center;`;
                $item.innerHTML = `<span style="opacity:0.9">${key}可以互动</span><span style="color:#4ade80; font-weight:bold; margin-left:12px; background:rgba(0,0,0,0.2); padding:2px 8px; border-radius:12px;">x${currentCounts[key]}</span>`;
                $content.appendChild($item);
            });
        }

        // --- 3. 触发跳过模式 (当前亮起的卡牌) ---
        if (isSkipMode && activeSkipCards.length > 0) {
            hasContent = true;
            const $title = document.createElement('div');
            $title.textContent = '请勾选要跳过的卡牌 (点击改名):';
            $title.style.cssText = 'font-size: 12px; color: #fbbf24; margin-bottom: 4px; padding-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.2);';
            $content.appendChild($title);

            activeSkipCards.forEach(cardData => {
                const isChecked = autoSkipCards.has(cardData.uniqueId);
                $content.appendChild(createCheckboxItem(cardData, isChecked));
            });
        }

        // --- 4. 常驻的已勾选列表 ---
        const inactiveSavedCards = Array.from(autoSkipCards.entries()).filter(([uniqueId]) => {
            if (!isSkipMode) return true;
            return !activeSkipCards.some(c => c.uniqueId === uniqueId);
        });

        if (inactiveSavedCards.length > 0) {
            if (hasContent) {
                const $divider = document.createElement('div');
                $divider.style.cssText = 'height: 1px; background: rgba(255,255,255,0.2); margin: 6px 0 2px 0;';
                $content.appendChild($divider);
            }
            hasContent = true;

            const $titleRow = document.createElement('div');
            $titleRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; margin-top: 4px;';
            $titleRow.innerHTML = `<div style="font-size: 12px; color: #9ca3af;">已设置自动跳过:</div>`;

            const $clearBtn = document.createElement('button');
            $clearBtn.textContent = '清空名单';
            $clearBtn.style.cssText = 'background: rgba(239, 68, 68, 0.2); border: 1px solid rgba(239, 68, 68, 0.5); color: #fca5a5; font-size: 11px; border-radius: 4px; cursor: pointer; padding: 2px 6px;';
            $clearBtn.addEventListener('click', () => { autoSkipCards.clear(); lastFingerprint = ""; });

            $titleRow.appendChild($clearBtn);
            $content.appendChild($titleRow);

            inactiveSavedCards.forEach(([uniqueId, cardData]) => {
                $content.appendChild(createCheckboxItem(cardData, true, true));
            });
        }

        $panel.style.display = hasContent ? 'flex' : 'none';
    }

    /**
     * 帧循环主函数
     */
    function frameTick() {
        frameCount++;
        if (frameCount % CONFIG.checkEveryNFrames !== 0) {
            requestAnimationFrame(frameTick);
            return;
        }

        const skipBtns = Array.from(document.querySelectorAll('button.skip-triggers-button'));
        const activeSkipBtn = skipBtns.find(btn => {
            if (btn.disabled) return false;
            const rect = btn.getBoundingClientRect();
            return rect.width > 0 && rect.height > 0;
        });

        const targetElements = document.querySelectorAll('[class*="can-interact"]');

        const interactEls = [];
        targetElements.forEach(el => {
            for (const cls of el.classList) {
                if (CONFIG.interactClassRegex.test(cls)) { interactEls.push(el); break; }
            }
        });

        const playerInfo = getPlayersInfo();
        let preventSkip = false;
        if (settings.neverSkipMyTurn && playerInfo.currentActivePlayer && playerInfo.currentActivePlayer === settings.myInvestigator) {
            preventSkip = true;
        }

        const isSkipMode = !!activeSkipBtn && interactEls.length > 0;
        const currentCounts = {};
        const activeSkipCards = [];

        if (isSkipMode) {
            interactEls.forEach(el => {
                const details = getCardDetails(el);
                activeSkipCards.push(details);
                if (details.baseId && !cardNames[details.baseId] && !fetchingIds.has(details.baseId)) {
                    fetchCardNameFromAPI(details.baseId);
                }
            });

            const allChecked = activeSkipCards.length > 0 && activeSkipCards.every(c => autoSkipCards.has(c.uniqueId));

            // 触发自动跳过的判定
            if (allChecked && !preventSkip && !isPaused) {
                const now = Date.now();
                // 基础防抖 (200ms) 确保不会在一瞬间点两次
                if (now - lastAutoClickTime > 200) {
                    clickHistory.push(now);
                    // 清理 1 秒之前的记录
                    clickHistory = clickHistory.filter(t => now - t <= 1000);

                    // 熔断机制:如果在1秒内出现了第3次点击,强制暂停
                    if (clickHistory.length >= 3) {
                        setPauseState(true);
                        clickHistory = []; // 清空记录,防止解禁后立即再次触发
                    } else {
                        activeSkipBtn.click();
                        lastAutoClickTime = now;
                    }
                }
            }
        } else {
            interactEls.forEach(el => {
                for (const cls of el.classList) {
                    const match = cls.match(CONFIG.interactClassRegex);
                    if (match) {
                        const key = match[1] || '通用';
                        currentCounts[key] = (currentCounts[key] || 0) + 1;
                        break;
                    }
                }
            });
        }

        const fingerprintData = {
            skipMode: isSkipMode,
            counts: currentCounts,
            active: activeSkipCards.map(c => c.uniqueId).join(','),
            saved: Array.from(autoSkipCards.keys()).join(','),
            names: JSON.stringify(cardNames),
            fetching: Array.from(fetchingIds).join(','),
            playerData: JSON.stringify(playerInfo),
            mySetting: settings.myInvestigator,
            noSkip: settings.neverSkipMyTurn,
            paused: isPaused // 暂停状态变更也强制刷新指纹
        };
        const currentFingerprint = JSON.stringify(fingerprintData);

        if (currentFingerprint !== lastFingerprint) {
            renderUnifiedPanel(isSkipMode, currentCounts, activeSkipCards, playerInfo, preventSkip);
            lastFingerprint = currentFingerprint;
        }

        requestAnimationFrame(frameTick);
    }

    function init() {
        createPanel();
        frameTick();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();