AutoClick for "Server Busy" ✧BETA✧

Автоматически нажимает кнопку "Повторить" при появлении сообщения "The server is busy. Please try again later."

// ==UserScript==
// @name          AutoClick for "Server Busy" ✧BETA✧
// @name:en       AutoClick for "Server Busy" ✧BETA✧
// @name:ru       АвтоКлик при "Сервер занят" ✧BETA✧
// @namespace     https://chat.deepseek.com
// @version       2.20
// @description      Автоматически нажимает кнопку "Повторить" при появлении сообщения "The server is busy. Please try again later."
// @description:en   Automatically clicks the "Retry" button when "The server is busy. Please try again later." message appears
// @author        KiberAndy + Ai
// @license       MIT
// @match         https://chat.deepseek.com/*
// @grant         none
// @icon          https://chat.deepseek.com/favicon.svg
// ==/UserScript==

(function() {
    'use strict';

    const DEBUG = false; // Установить в true для подробных логов

    const log = (...args) => {
        if (DEBUG) {
            console.log('%c[ASB]%c', 'color: orange; font-weight: bold;', 'color: unset;', ...args);
        }
    };
    const errorLog = (...args) => { // Ошибки логируем всегда, если не DEBUG то без префикса
        if (DEBUG) {
            console.error('%c[ASB Error]%c', 'color: red; font-weight: bold;', 'color: unset;', ...args);
        } else {
            console.error(...args);
        }
    };

    const busyMessageText = "Server busy, please try again later.";
    const regenerateButtonText = "重新生成";
    const continueButtonText = "Continue";

    const messageRetryState = new WeakMap();

    function isElementVisible(element, elementName = 'Element') {
        if (DEBUG) log(`isElementVisible: Checking visibility for ${elementName}`, element);

        if (!element) {
           if (DEBUG) log(`isElementVisible [FAIL]: ${elementName} is null.`);
           return false;
        }
        if (!document.body.contains(element)) {
           if (DEBUG) log(`isElementVisible [FAIL]: ${elementName} is not in DOM.`, element);
           return false;
        }

        const style = getComputedStyle(element);
        if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
            if (DEBUG) log(`isElementVisible [FAIL]: ${elementName} hidden by style (display/visibility/opacity). D: ${style.display}, V: ${style.visibility}, O: ${style.opacity}`);
            return false;
        }

        // offsetParent check can be tricky, if display is not none, it might still be "visible" (e.g. fixed)
        // So, we rely more on display, visibility, opacity and dimensions.
        if (element.offsetParent === null && style.display !== 'fixed' && style.display !== 'sticky') {
             // If display is 'none', previous check would catch it.
             // This checks for cases where element is detached in a way that might make it non-interactive.
             // For `<span>` elements, `offsetParent` can be its parent block element.
             // If `elementName` is "Busy Message Span" and it's truly visible, its `offsetParent` should not be `null` unless it's `position:fixed/sticky`.
             if (DEBUG) log(`isElementVisible [INFO]: ${elementName} has offsetParent null and is not fixed/sticky. Display: ${style.display}`);
        }


        const rect = element.getBoundingClientRect();
        if (rect.width === 0 && rect.height === 0) {
            if (DEBUG) log(`isElementVisible [FAIL]: ${elementName} has zero width AND zero height.`, rect);
            return false;
        }
        // Check if in viewport - this is a "soft" check, can be removed if too restrictive
        // const inViewport = rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
        // if (!inViewport) {
        //    if (DEBUG) log(`isElementVisible [WARN]: ${elementName} is outside viewport. This might be okay.`);
        // }

        if (DEBUG) log(`isElementVisible [PASS]: ${elementName} is considered VISIBLE.`);
        return true;
    }

    function simulateClick(element) {
        if (!element) {
             errorLog("simulateClick called with null element.");
             return;
        }
        log('Attempting to simulate click on:', element);
        const options = { bubbles: true, cancelable: true, view: window, clientX: 1, clientY: 1 };
        try {
            element.dispatchEvent(new PointerEvent('pointerdown', options));
            element.dispatchEvent(new MouseEvent('mousedown', options));
            element.dispatchEvent(new PointerEvent('pointerup', options));
            element.dispatchEvent(new MouseEvent('mouseup', options));
            element.dispatchEvent(new MouseEvent('click', options));
            if (typeof element.focus === 'function') element.focus();
            // Нативный click() может быть нужен для некоторых элементов
            if (typeof element.click === 'function' && !element.closest('svg')) {
                 log('Additionally calling native .click() method on element:', element);
                 element.click();
            }
            log('Simulated click event sequence successfully dispatched on element:', element);
        } catch (e) {
            errorLog("Error during advanced click simulation:", e, "on element:", element);
            log('Falling back to basic native click.');
            try {
                element.click();
                log('Basic native click() called on element:', element);
            } catch (nativeClickError) {
                errorLog("Error with basic native click():", nativeClickError, "on element:", element);
            }
        }
    }

    function findRetryButton(messageSpanElement) {
        log('findRetryButton: Initiated for message span:', messageSpanElement);
        // Метод 0: Прямой структурный поиск для новой разметки кнопки "Обновить"
        try {
            const messageInnerContainer = messageSpanElement.parentElement; // span -> div.ac2694a7
            if (messageInnerContainer && messageInnerContainer.classList.contains('ac2694a7')) {
                const messageOuterContainer = messageInnerContainer.parentElement; // div.ac2694a7 -> div.e13328ad
                if (messageOuterContainer && messageOuterContainer.classList.contains('e13328ad')) {
                    // В логах мы видели, что previousElementSibling от div.e13328ad это div.fbb737a4,
                    // который содержит и текст "Теперь мышь работает..." и кнопку.
                    // А div._9663006 является родителем для div.fbb737a4.
                    // Попробуем сначала найти кнопку в непосредственном соседе, а потом в его родителе.

                    let buttonSearchContexts = [];
                    const directPreviousSibling = messageOuterContainer.previousElementSibling; // Ожидаем div.fbb737a4 или div._9663006

                    if (directPreviousSibling && directPreviousSibling.nodeType === Node.ELEMENT_NODE) {
                        buttonSearchContexts.push(directPreviousSibling);
                        // Если прямой сосед НЕ _9663006, то возможно _9663006 это его родитель
                        if (!directPreviousSibling.classList.contains('_9663006') && directPreviousSibling.parentElement && directPreviousSibling.parentElement.classList.contains('_9663006')) {
                           // Эта ветка не должна сработать судя по логам, но на всякий случай
                           // buttonSearchContexts.push(directPreviousSibling.parentElement);
                        } else if (directPreviousSibling.classList.contains('_9663006')) {
                            // Если прямой сосед это _9663006, то искать надо внутри него, возможно в .fbb737a4
                            const innerContext = directPreviousSibling.querySelector('.fbb737a4');
                            if (innerContext) buttonSearchContexts.unshift(innerContext); // Искать сначала в .fbb737a4
                        }
                    }


                    for (const buttonHostBlock of buttonSearchContexts) {
                        log('findRetryButton (M0): Searching in buttonHostBlock:', buttonHostBlock);

                        const specificSvgPathStart = "M12 .5C18.351.5";
                        const buttonSelectors = [
                            // Ищем по уникальному SVG path кнопки "Обновить"
                            `div.ds-icon-button svg path[d^="${specificSvgPathStart}"]`,
                            // Резервные варианты, если path изменится
                            'div.ds-icon-button[tabindex="0"]',
                            'div.ds-icon-button'
                        ];

                        for (const selector of buttonSelectors) {
                            log(`findRetryButton (M0): Attempting selector "${selector}" within buttonHostBlock.`);
                            let foundElement = buttonHostBlock.querySelector(selector);

                            if (foundElement) {
                                const actualButton = foundElement.tagName.toLowerCase() === 'path' ? foundElement.closest('div.ds-icon-button') : foundElement;
                                if (actualButton && actualButton.classList.contains('ds-icon-button')) {
                                    if (isElementVisible(actualButton, 'Method 0 Button')) {
                                        log('Retry Button Found and VISIBLE (Method 0 - New Structure):', actualButton);
                                        return actualButton;
                                    } else {
                                        log('findRetryButton (M0): Button found by Method 0 but NOT visible:', actualButton);
                                    }
                                }
                            }
                        }
                    }
                     log('findRetryButton (M0): No visible button found via structural search in identified contexts.');
                }
            }
        } catch(e) {
            errorLog("Error in Method 0 (Structural for new Refresh button):", e);
        }
        log('Method 0 did not find the button. Falling back to generic methods.');

        // --- Fallback methods (для кнопки Regenerate или если структура сильно изменится) ---
        const searchScopeForFallbacks = messageSpanElement.closest('div.chat-message-container, div.message-container, main, body') || document.body; // Более широкий, но релевантный поиск
        log('findRetryButton (Fallback): Search scope:', searchScopeForFallbacks);
        // ... (остальные фолбэк методы можно оставить как были, или упростить, если они не нужны)

        // Метод 1 (Старый): Поиск по SVG ID "重新生成" (для кнопки Regenerate ответа ИИ)
        const specificSvgElementById = searchScopeForFallbacks.querySelector('#\\u91CD\\u65B0\\u751F\\u6210');
        if (specificSvgElementById) {
            let potentialButtonContainer = specificSvgElementById.closest('button, div.ds-icon-button');
            if (potentialButtonContainer && isElementVisible(potentialButtonContainer, 'Method 1 Button')) {
                log('Retry/Regenerate Button Found (Method 1 - SVG ID) and VISIBLE:', potentialButtonContainer);
                return potentialButtonContainer;
            }
        }
        log('findRetryButton: No suitable button found after ALL methods.');
        return null;
    }


    function checkAndClick() {
        if (DEBUG) log('---------------- ciclo ----------------');
        log('checkAndClick: Cycle started.');

        const potentialMessageSpans = document.querySelectorAll('div.e13328ad div.ac2694a7 span');
        let latestMessageElementThisCycle = null;

        for (const span of potentialMessageSpans) {
            const spanText = span.textContent?.trim();
            if (spanText === busyMessageText) {
                log(`checkAndClick: Found span with matching busyMessageText ("${busyMessageText}")`);
                if (isElementVisible(span, 'Busy Message Span')) {
                    log('checkAndClick: Matched span IS VISIBLE:', span);
                    latestMessageElementThisCycle = span;
                } else {
                    log('checkAndClick: Matched span IS NOT VISIBLE. Skipping this one.');
                }
            }
        }

        if (latestMessageElementThisCycle) {
             log('checkAndClick: "Server is busy" message IS active and visible.');
             if (!messageRetryState.has(latestMessageElementThisCycle)) {
                 log('checkAndClick: New "Server is busy" instance. Initializing state.');
                 messageRetryState.set(latestMessageElementThisCycle, { buttonClicked: false, timestamp: Date.now() });
             }

             const currentState = messageRetryState.get(latestMessageElementThisCycle);
             if (currentState.buttonClicked) {
                 log('checkAndClick: Retry button ALREADY CLICKED for this message instance. Skipping.');
             } else {
                 const retryButtonElement = findRetryButton(latestMessageElementThisCycle);
                 if (retryButtonElement) {
                     log('checkAndClick: Retry button element candidate found:', retryButtonElement);
                     const buttonVisible = isElementVisible(retryButtonElement, 'Final Retry Button Check');
                     let buttonClickable = false;
                     if (buttonVisible) {
                         const style = getComputedStyle(retryButtonElement);
                         buttonClickable = style.pointerEvents !== 'none' && style.cursor !== 'default' && style.cursor !== 'auto' && style.cursor !== 'not-allowed';
                         log('checkAndClick: Retry button pointerEvents:', style.pointerEvents, 'Cursor:', style.cursor, '=> Clickable:', buttonClickable);
                     }

                     if (buttonVisible && buttonClickable) {
                         log('checkAndClick: Retry button is VISIBLE and CLICKABLE. Attempting click.');
                         simulateClick(retryButtonElement);
                         currentState.buttonClicked = true;
                         log('checkAndClick: Clicked Retry/Refresh. Marked state as clicked.');
                     } else {
                         log('checkAndClick: Retry button NOT clickable or NOT visible (Final Check). Will try next cycle.', 'Visible:', buttonVisible, 'Clickable:', buttonClickable);
                     }
                 } else {
                     log('checkAndClick: Retry button NOT found by findRetryButton. Will try next cycle.');
                 }
             }
        }

        // --- Кнопка "Continue" ---
        const potentialContinueButtons = document.querySelectorAll('div[role="button"].ds-button.ds-button--secondary.ds-button--bordered.ds-button--rect.ds-button--m');
        let continueButtonToClick = null;
        for (const btn of potentialContinueButtons) {
            if (isElementVisible(btn, 'Continue Button Candidate') && btn.textContent?.trim() === continueButtonText) {
                continueButtonToClick = btn;
                break;
            }
        }
        if (continueButtonToClick) {
            const style = getComputedStyle(continueButtonToClick);
            const isClickable = style.pointerEvents !== 'none' && style.cursor !== 'default' && style.cursor !== 'auto' && style.cursor !== 'not-allowed';
            if (isClickable) {
                log(`"${continueButtonText}" button is clickable. Attempting to click.`);
                simulateClick(continueButtonToClick);
            }
        }
        log('checkAndClick: Cycle ended.');
    }

    const checkInterval = setInterval(checkAndClick, 2500); // Можно вернуть интервал к 2-2.5с
    console.log('%c[ASB]%c Script "AutoClick (Server is busy) + Auto-Click Continue (Improved Retry)" v2.20 запущен.', 'color: orange; font-weight: bold;', 'color: unset;');

})();