YouTube — Auto Switch Live Chat

Automatically switch YouTube live chat to "All Messages" and supports Replay Chat. Optimized for high discoverability and low performance impact.

// ==UserScript==
// @name          YouTube — Auto Switch Live Chat
// @name:zh-CN    YouTube — 自动切换直播聊天室
// @name:zh-TW    YouTube — 自動切換即時聊天室
// @name:ja       YouTube — ライブチャット自動切替
// @name:ko       YouTube — 실시간 채팅 자동 전환
// @name:es       YouTube — Cambio automático de chat en vivo
// @name:fr       YouTube — Commutateur automatique de chat en direct
// @name:de       YouTube — Automatischer Live-Chat-Wechsel
// @name:it       YouTube — Cambio automatico della chat dal vivo
// @name:ru       YouTube — Автоматическое переключение чата
// @name:pt       YouTube — Alternância automática de chat ao vivo
// @name:nl       YouTube — Automatisch Live Chat Schakelen
// @namespace     greasyfork-Auto Switch Live Chat
// @version       1.3.9
// @description    Automatically switch YouTube live chat to "All Messages" and supports Replay Chat. Optimized for high discoverability and low performance impact.
// @description:zh-CN 自动将 YouTube 直播聊天切换到「所有消息」並支援重播聊天室。已優化可發現性與效能。
// @description:zh-TW 自動將 YouTube 即時聊天切換到「所有訊息」並支援重播聊天室。已優化可發現性與效能。
// @description:ja YouTubeライブチャットを自動で「すべてのチャット」に切替、リプレイチャット対応。検出性とパフォーマンスを最適化。
// @description:ko YouTube 실시간 채팅을 자동으로 "모든 메시지"로 전환하며 다시보기 채팅 지원. 감지 및 성능 최적화.
// @description:es Cambia automáticamente el chat en vivo de YouTube a "Todos los mensajes" y soporta Chat de Repetición. Optimizado para alta detección y bajo impacto de rendimiento.
// @description:fr Bascule automatiquement le chat en direct de YouTube sur "Tous les messages" et supporte Chat de Rediffusion. Optimisé pour une haute détectabilité et un faible impact sur les performances.
// @description:de Schaltet den YouTube Live-Chat automatisch auf "Alle Nachrichten" um und unterstützt Replay-Chat. Optimiert für hohe Auffindbarkeit und geringe Leistungsauswirkungen.
// @description:it Passa automaticamente la chat dal vivo di YouTube a "Tutti i messaggi" e supporta Chat di Replay. Ottimizzato per l'alta rilevabilità e il basso impatto sulle prestazioni.
// @description:ru Автоматически переключает чат YouTube на "Все сообщения" и поддерживает чат повтора. Оптимизирован для высокой обнаруживаемости и низкого воздействия на производительность.
// @description:pt Alterna automaticamente o chat ao vivo do YouTube para "Todas as mensagens" e suporta o chat de repetição. Otimizado para alta detecção e baixo impacto no desempenho.
// @description:nl Schakelt YouTube live chat automatisch naar "Alle berichten" en ondersteunt Replay Chat. Geoptimaliseerd voor hoge vindbaarheid en lage prestatie-impact.
// @license        MIT
// @author         凝流
// @match          https://www.youtube.com/*
// @icon           https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant none
// @run-at document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ==================== 配置 / Configuration ====================
    // - 腳本運作的參數配置 (Script behavior parameters)
    const CONFIG = {
        MAX_RETRIES: 25,
        RETRY_INTERVAL: 1000,
        OBSERVER_TIMEOUT: 6000, // 用於所有等待超時 (Used for all waiting timeouts)
        SPA_DELAY: 300, // 僅用於初始掃描 (Only for initial scan)
        DEBUG: true // 設為 true 可查看詳細日誌 (Set to true for detailed logs)
    };

    // ==================== 多語言關鍵字 / Multi-language Keywords ====================
    // - 原始關鍵字 (Raw keywords)
    const _KEYWORDS_RAW = {
        // 點擊開啟選單的按鈕文字 (Text for button to open the menu)
        button: [
            // --- Live Chat Keywords ---
            'top chat', 'top-chat', // English
            '重點', '直播聊天', // Traditional Chinese
            '重点', '直播聊天室', '重要消息', // Simplified Chinese
            'トップチャット', 'チャット', // Japanese
            '주요 채팅', // Korean
            'chat destacado', 'principal', // Spanish
            'Principais mensagens', // Portuguese
            'Интересные сообщения', // Russian
            'Top Chat', // General
            'Messaggi principali', // Italian
            'Top-Chat', 'Top-Chat-Meldungen', // German

            // --- Replay Chat Keywords ---
            'top replay chat', 'top messages replay', // English
            '重播熱門聊天室訊息', // Traditional Chinese
            '重播热门聊天室消息', '重播重要消息', // Simplified Chinese
            '上位のチャットのリプレイ', // Japanese
            '주요 다시보기 채팅', '다시보기 주요 메시지', // Korean
            'chat destacado de repetición', 'mensajes destacados de repetición', // Spanish
            "revoir l'essentiel du chat", // French
            'top-chat-wiedergabe', 'top-chat-nachrichten-wiedergabe', // German
            'chat di replay', 'messaggi principali di replay', // Italian
            'только интересные сообщения', // Russian
            'principais mensagens de reprise', 'chat de reprise', // Portuguese
        ],

        // 選單中「顯示所有訊息」的選項文字 (Menu option text for "All Messages")
        menu: [
            // --- Live Chat Keywords ---
            'all messages', 'live chat', // English
            '所有訊息', '即時聊天', '聊天室', // Traditional Chinese
            '所有消息', '实时聊天', // Simplified Chinese
            'すべてのチャット', 'チャット', // Japanese
            '실시간 채팅', // Korean
            'Chat en directo', 'todos los mensajes', // Spanish
            'Chat em direto', // Portuguese
            'все сообщения', 'Чат', // Russian
            'Chat en direct', // French
            'alle chats', // Dutch
            'Chat live', // General
            'Alle Nachrichten', // German
            'tutti i messaggi', // Italian

            // --- Replay Chat Keywords ---
            'live chat replay', 'all messages replay', // English
            '聊天重播', '你可以查看所有訊息', // Traditional Chinese
            '你可以查看所有消息', // Simplified Chinese
            'チャットのリプレイ', // Japanese
            '실시간 채팅 다시보기', '모든 메시지 다시보기', // Korean
            'chat en directo de repetición', 'todos los mensajes de repetición', // Spanish
            'replay du chat en direct', // French
            'alle nachrichten wiedergabe', 'live-chat-wiedergabe', // German
            'tutti i messaggi di replay', // Italian
            'запись чата', // Russian
            'todas as mensagens de reprise', 'chat ao vivo de reprise', // Portuguese
        ],

        // 排除關鍵字(避免誤點選單中相同的關鍵字) (Exclude keywords to avoid misclicks)
        exclude: [
            // --- Live Chat Exclude ---
            'top chat', // English
            '重點', '重要消息', // Traditional/Simplified Chinese
            'chat destacado', // Spanish
            'лучший чат', 'Интересные сообщения', // Russian
            'トップチャット', // Japanese
            'Top Chat', // General
            'Principais mensagens', // Portuguese
            'Messaggi principali', // Italian
            '주요 채팅', // Korean
            'Top-Chat', 'Top-Chat-Meldungen', // German

            // --- Replay Chat Exclude ---
            'top replay chat', 'top messages replay', // English
            '重播熱門聊天室訊息', // Traditional Chinese
            '重播重要消息', // Simplified Chinese
            '上位のチャットのリプレイ', // Japanese
            '주요 다시보기 채팅', // Korean
            'chat destacado de repetición', // Spanish
            "revoir l'essentiel du chat", // French
            'top-chat-wiedergabe', // German
            'messaggi principali di replay', // Italian
            'только интересные сообщения', // Russian
            'principais mensagens de reprise', // Portuguese
        ]
    };

    // - 正規化工具 (Normalization utility)
    const normalize = (str) => {
        if (!str) return '';
        // 統一處理大小寫、變音符號、各種空白字元、引號和破折號
        return String(str)
            .toLowerCase()
            .normalize('NFKD') // 處理變音符號
            .replace(/[\u0300-\u036f]/g, '') // 移除重音 (例如法語 l'essentiel)
            .replace(/[\s\u00A0\u200B\u2009\u202F\uFEFF]+/g, '') // 移除各類空白字元
            .replace(/[’'`"“”«»\-–—‑]+/g, ''); // 移除常見引號與各類破折號
    };

    // - 預先正規化的關鍵字 (Pre-normalized keywords for performance)
    const KEYWORDS = {
        button_norm: _KEYWORDS_RAW.button.map(normalize),
        menu_norm: _KEYWORDS_RAW.menu.map(normalize),
        exclude_norm: _KEYWORDS_RAW.exclude.map(normalize)
    };

    // ==================== 狀態管理 / State Management ====================

    class StateManager {
        constructor() {
            this.currentNavId = Date.now();
            this.processedFrames = new Map();
            this.processedDocs = new Map();
            this.activeObservers = new Set();
            this.activeTimers = new Set();
        }

        reset() {
            this.activeObservers.forEach(obs => {
                try {
                    obs.disconnect();
                } catch (e) {}
            });
            this.activeObservers.clear();

            this.activeTimers.forEach(timer => {
                try {
                    clearTimeout(timer);
                } catch (e) {}
            });
            this.activeTimers.clear();

            this.processedFrames.clear();
            this.processedDocs.clear();

            this.currentNavId = Date.now();
        }

        addObserver(observer) {
            this.activeObservers.add(observer);
            return observer;
        }

        addTimer(callback, delay) {
            const timer = setTimeout(() => {
                this.activeTimers.delete(timer);
                try {
                    callback();
                } catch (e) {
                    console.error('[YT Chat] Timer callback error:', e);
                }
            }, delay);
            this.activeTimers.add(timer);
            return timer;
        }

        clearTimer(timer) {
            if (this.activeTimers.has(timer)) {
                this.activeTimers.delete(timer);
                clearTimeout(timer);
            }
        }

        isProcessed(element, isFrame = false) {
            if (!element) return true;
            const map = isFrame ? this.processedFrames : this.processedDocs;
            return map.has(element);
        }

        markProcessed(element, isFrame = false) {
            if (!element) return;
            const map = isFrame ? this.processedFrames : this.processedDocs;
            map.set(element, this.currentNavId);
        }

        isValidNavId(navId) {
            return navId === this.currentNavId;
        }
    }

    const state = new StateManager();

    // ==================== 日誌系統 / Logging System ====================
    const LOG_PREFIX = '[YT Chat]';
    const LOG_STYLE = {
        init: 'color: #4CAF50; font-weight: bold',
        success: 'color: #1E88E5',
        warn: 'color: #FF9800',
        error: 'color: #F44336'
    };

    function log(type, msgFn) {
        if (!CONFIG.DEBUG && type !== 'error') return;
        const style = LOG_STYLE[type] || '';
        const message = typeof msgFn === 'function' ? msgFn() : msgFn;
        console.log(`%c${LOG_PREFIX}`, style, message);
    }

    // ==================== 核心函數 / Core Functions ====================

    // - 模擬點擊事件 (Simulate a click event robustly)
    function simulateClick(element) {
        if (!element) return false;

        // 使用元素所在 document 的 window 作為 view
        const view = element.ownerDocument?.defaultView || window;

        // 1. 嘗試原生 click()
        try {
            if (typeof element.click === 'function') {
                element.click();
                return true;
            }
        } catch (e) {
            // Fallback below
        }

        // 2. 嘗試 MouseEvent 模擬
        try {
            const evt = new view.MouseEvent('click', { bubbles: true, cancelable: true });
            return element.dispatchEvent(evt);
        } catch (e) {
            return false;
        }
    }

    // - 等待元素出現 (Wait for an element to appear)
    function waitForElement(doc, selector, timeout, navId) {
        return new Promise(resolve => {
            if (!state.isValidNavId(navId)) {
                resolve(null);
                return;
            }

            const existing = doc.querySelector(selector);
            if (existing) {
                log('init', () => `✓ Found element: ${selector}`);
                resolve(existing);
                return;
            }

            log('init', () => `⏳ Waiting for element: ${selector}...`);

            let resolved = false;

            const observer = new MutationObserver(() => {
                if (resolved) return;

                if (!state.isValidNavId(navId)) {
                    resolved = true;
                    observer.disconnect();
                    log('warn', () => `⚠ Navigation changed while waiting for: ${selector}`);
                    resolve(null);
                    return;
                }

                const el = doc.querySelector(selector);
                if (el) {
                    resolved = true;
                    state.clearTimer(timerId);
                    observer.disconnect();
                    log('init', () => `✓ Found element: ${selector}`);
                    resolve(el);
                }
            });

            const timerId = state.addTimer(() => {
                if (!resolved) {
                    resolved = true;
                    observer.disconnect();
                    log('warn', () => `⚠ Timeout waiting for element: ${selector}`);
                    resolve(null);
                }
            }, timeout);

            // - doc.body 重試機制 (Retry mechanism for doc.body)
            function attemptObserve() {
                if (!state.isValidNavId(navId) || resolved) return;

                if (doc.body) {
                    state.addObserver(observer);
                    observer.observe(doc.body, { childList: true, subtree: true });
                } else {
                    log('warn', '⚠ Document body not yet available, retrying observer attachment...');
                    state.addTimer(attemptObserve, 100); // Short retry
                }
            }

            attemptObserve();
        });
    }

    // - 根據關鍵字點擊按鈕 (Click a button based on keywords)
    function clickByKeywords(container, normalizedKeywords, type) {
        if (!container) {
            log('error', `✗ Container is null for ${type} click.`);
            return false;
        }

        const elements = container.querySelectorAll(
            'button, tp-yt-paper-button, [role="button"]'
        );

        for (const el of elements) {
            const textSources = [
                el.innerText,
                el.textContent,
                el.getAttribute('aria-label'),
                el.title,
                el.dataset.tooltip,
                el.getAttribute('data-tooltip-text')
            ];

            const text = textSources.find(t => t && String(t).trim())?.toString() || '';
            const normalizedText = normalize(text);

            if (normalizedText && normalizedKeywords.some(k => normalizedText.includes(k))) {
                log('init', () => `✓ Clicking ${type} button: "${text.trim()}"`);

                if (simulateClick(el)) {
                    return true;
                } else {
                    log('error', `✗ Failed to click ${type} button via simulation.`);
                    return false;
                }
            }
        }

        log('warn', () => `⚠ ${type} button not found.`);
        return false;
    }

    // - 監聽並點擊選單項目 (Observe and click a menu item)
    // ** v1.3.7 優化:使用 MutationObserver 替換輪詢 **
    function pollMenuItems(container, normalizedKeywords, normalizedExcludeKeywords, timeout, navId) {
        return new Promise(resolve => {
            if (!container) {
                log('error', '✗ Menu container is null.');
                resolve(false);
                return;
            }

            let resolved = false;
            let observer; // 宣告在外部,以便在 checkAndClick 中斷開
            let timerId; // 宣告在外部,以便在 checkAndClick 中清除

            // --- 核心檢查及點擊邏輯 (Core Check and Click Logic) ---
            const checkAndClick = () => {
                if (resolved) return;

                // 1. SPA 健壯性檢查
                if (!state.isValidNavId(navId)) {
                    resolved = true;
                    if (observer) observer.disconnect();
                    state.clearTimer(timerId);
                    log('warn', () => `⚠ Observer aborted: navigation changed.`);
                    resolve(false);
                    return;
                }

                // 2. 搜尋所有可能的選單項目
                const items = container.querySelectorAll(
                    'tp-yt-paper-item, yt-live-chat-menu-item-renderer, [role="menuitem"]'
                );

                for (const item of items) {
                    const textSources = [
                        item.innerText,
                        item.textContent,
                        item.getAttribute('aria-label'),
                        item.title,
                        item.getAttribute('data-tooltip-text')
                    ];
                    const text = textSources.find(t => t && String(t).trim())?.toString() || '';
                    const normalizedText = normalize(text);

                    if (!normalizedText) continue;

                    // 排除檢查 (Exclude check)
                    if (normalizedExcludeKeywords.some(k => normalizedText.includes(k))) {
                        continue;
                    }

                    // 目標檢查 (Target check)
                    if (normalizedKeywords.some(k => normalizedText.includes(k))) {
                        const isSelected =
                            item.hasAttribute('selected') ||
                            item.getAttribute('aria-selected') === 'true' ||
                            item.getAttribute('aria-checked') === 'true' ||
                            item.classList.contains('iron-selected') ||
                            item.classList.contains('selected');

                        if (isSelected) {
                            log('success', () => `✓ Already selected: "${text.trim()}"`);
                        } else {
                            // 點擊目標 (Click target)
                            log('success', () => `✓ Switched to: "${text.trim()}"`);
                            simulateClick(item);
                        }

                        // **清理與解決 (Cleanup and Resolve)**:無論是否已選中,任務都已完成。
                        resolved = true;
                        if (observer) observer.disconnect();
                        state.clearTimer(timerId);
                        resolve(true);
                        return;
                    }
                }
            };
            // --- 核心檢查及點擊邏輯結束 ---

            // 3. 設定超時計時器 (Set timeout timer)
            timerId = state.addTimer(() => {
                if (!resolved) {
                    resolved = true;
                    if (observer) observer.disconnect(); // 超時時停止觀察者
                    log('error', '✗ Menu item not found (Observer Timeout).');
                    resolve(false);
                }
            }, timeout);

            // 4. 設定 MutationObserver (Set MutationObserver)
            observer = state.addObserver(new MutationObserver(checkAndClick));

            // 5. 開始監聽 (Start observing)
            observer.observe(container, { childList: true, subtree: true });

            // 6. 立即執行一次,以防選單項目在監聽開始前已經存在
            checkAndClick();
        });
    }

    // - 執行聊天切換 (Perform the chat switch)
    async function switchChat(doc, navId) {
        if (!state.isValidNavId(navId)) {
            log('warn', () => `⚠ switchChat aborted: navigation changed.`);
            return;
        }

        const isIframe = (doc !== document);

        if (state.isProcessed(doc, false)) {
            log('init', () => isIframe ? '→ iframe already processed.' : '→ Document already processed.');
            return;
        }

        state.markProcessed(doc, false);

        log('init', () => isIframe ?
            `▶ Starting chat switch in iframe.` :
            `▶ Starting chat switch in main page.`);

        const header = await waitForElement(
            doc,
            'yt-live-chat-header-renderer',
            CONFIG.OBSERVER_TIMEOUT,
            navId
        );

        if (!header) {
            log('error', '✗ Chat header not found.');
            return;
        }

        // 點擊開關按鈕 (Click switch button)
        if (!clickByKeywords(header, KEYWORDS.button_norm, 'switch')) {
            log('error', '✗ Could not click switch button.');
            return;
        }

        // 等待選單容器
        const menu = await waitForElement(
            doc,
            '[role="menu"], [role="listbox"]',
            CONFIG.OBSERVER_TIMEOUT,
            navId
        );

        if (!menu) {
            log('error', '✗ Menu failed to appear.');
            return;
        }

        // 監聽並點擊選單項目 (Observe and click menu item)
        await pollMenuItems(
            menu,
            KEYWORDS.menu_norm,
            KEYWORDS.exclude_norm,
            CONFIG.OBSERVER_TIMEOUT, // 使用 OBSERVER_TIMEOUT 作為選單出現的超時時間
            navId
        );
    }

    // - 初始化 iframe (Initialize an iframe)
    function initIframe(iframe, navId) {
        if (!state.isValidNavId(navId)) return;
        if (state.isProcessed(iframe, true)) return;

        state.markProcessed(iframe, true);
        log('init', () => `→ Initializing iframe.`);

        let retries = 0;

        function tryAccess() {
            if (!state.isValidNavId(navId)) {
                log('warn', () => `⚠ tryAccess aborted: navigation changed.`);
                return;
            }

            if (retries++ >= CONFIG.MAX_RETRIES) {
                log('error', () => `✗ Max retries (${CONFIG.MAX_RETRIES}) reached for iframe.`);
                return;
            }

            let doc;
            try {
                doc = iframe.contentDocument || iframe.contentWindow?.document;
            } catch (e) {
                if (e.name === 'SecurityError') {
                    log('error', `✗ iframe blocked by Same-Origin Policy. Script cannot access content.`);
                    // SecurityError is fatal; do not retry.
                    return;
                } else {
                    log('warn', () => `⚠ iframe access error (retry ${retries}/${CONFIG.MAX_RETRIES}): ${e.message}`);
                    state.addTimer(tryAccess, CONFIG.RETRY_INTERVAL);
                }
                return;
            }

            if (!doc || doc.readyState === 'loading') {
                log('warn', () => `⚠ iframe not ready (retry ${retries}/${CONFIG.MAX_RETRIES}).`);
                state.addTimer(tryAccess, CONFIG.RETRY_INTERVAL);
                return;
            }

            switchChat(doc, navId);
        }

        tryAccess();
    }

    // ==================== 啟動腳本 / Script Initialization (修改處) ====================

    // - 處理頁面加載/導航 (Handle page load/navigation)
    function startProcessing() {
        const navId = state.currentNavId;

        // 腳本啟動日誌 (Script startup log)
        log('init', '═══════════════════════════════════════');
        log('init', 'YouTube Auto Switch Live Chat v1.3.8 (Optimized)'); // <-- 日誌版本更新
        log('init', '═══════════════════════════════════════');

        const isLiveChatPage = location.pathname.startsWith('/live_chat');
        // 檢查當前是否在影片觀看頁面 (這是 Live Chat Iframe 的容器)
        const isVideoPage = document.querySelector('ytd-watch-flexy, ytd-watch-grid');

        // --- 【效能優化:快速退出檢查 (Quick Exit Check)】---
        // 如果既不是 live_chat 頁面,也不是影片觀看頁面 (如首頁、訂閱頁),則立即退出
        if (!isLiveChatPage && !isVideoPage) {
            log('warn', () => 'Quick Exit: Not on a video page or live chat page. Aborting processing to save performance.');
            return;
        }
        // --- 【快速退出檢查結束】---

        // 處理內嵌聊天室頁面 (Handle embedded chat page)
        if (isLiveChatPage) {
            log('init', () => `→ Embedded Chat Page detected. Running switchChat on document.`);
            switchChat(document, navId);
            return;
        }

        // 處理主要觀看頁面 (Handle main video page) - 只有在 isVideoPage 為 true 時才會執行到這裡
        log('init', () => `→ Main Video Page detected. Starting MutationObserver.`);

        const observer = state.addObserver(new MutationObserver(mutations => {
            if (!state.isValidNavId(navId)) {
                observer.disconnect();
                return;
            }

            // 監聽新加入的 iframe (Observe for newly added iframes)
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeName === 'IFRAME') {
                        const src = node.src || '';
                        const srcdoc = node.srcdoc || '';
                        if (src.includes('live_chat') || srcdoc.includes('live_chat')) {
                            log('init', () => '→ New chat iframe detected.');
                            initIframe(node, navId);
                        }
                    } else if (node.querySelectorAll) {
                        // 檢查新增節點的子樹中是否包含 iframe
                        node.querySelectorAll('iframe[src*="live_chat"], iframe[srcdoc*="live_chat"]')
                            .forEach(iframe => {
                                log('init', () => '→ Chat iframe detected in added node.');
                                initIframe(iframe, navId);
                            });
                    }
                });
            });
        }));

        // 決定觀察的目標 (Determine observe target)
        // 優先監聽聊天容器,如果找不到則監聽 body
        const chatContainer = document.querySelector('#chat, #chat-container, #chatframe');
        const observeTarget = chatContainer || document.body;

        if (observeTarget && state.isValidNavId(navId)) {
            // 監聽聊天容器或 body,以捕捉 iframe 的出現
            observer.observe(observeTarget, { childList: true, subtree: true });
        }

        // 初始掃描 (Initial scan for existing iframes)
        state.addTimer(() => {
            if (!state.isValidNavId(navId)) return;
            log('init', () => `→ Initial scan for existing iframes.`);
            document.querySelectorAll('iframe[src*="live_chat"], iframe[srcdoc*="live_chat"]')
                .forEach(iframe => initIframe(iframe, navId));
        }, CONFIG.SPA_DELAY);
    }

    // - 腳本初始化 (Script Initialization)
    function init() {
        // 初始執行 (Initial execution)
        startProcessing();

        // 監聽 SPA 導航 (Listen for SPA navigation)
        // 使用旗標避免重複綁定 (Use flag to prevent duplicate binding)
        if (!window._ytAutoChatBound) {
            window._ytAutoChatBound = true;
            window.addEventListener('yt-navigate-finish', () => {
                log('init', () => '→ SPA navigation detected.');
                state.reset(); // 重置狀態
                startProcessing(); // 立即重新執行
            });
        }
    }

    init();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址