AutoClick (Server is busy) ✧BETA✧

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

// ==UserScript==
// @name          AutoClick (Server is busy) ✧BETA✧
// @name:en       AutoClick for "Server Busy" ✧BETA✧
// @name:ru       АвтоКлик при "Сервер занят" ✧BETA✧
// @namespace     https://chat.deepseek.com
// @version       2.13
// @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 busyMessageText = "The server is busy. Please try again later.";
    const retryButtonText = "Повторить"; // Russian text
    const regenerateButtonText = "重新生成"; // Chinese text from the SVG ID (same as rect ID)

    // Переменные для отслеживания состояния по каждому сообщению
    let trackingMessageElement = null; // Ссылка на последний ОБРАБОТАННЫЙ элемент сообщения "Server is busy" (самый нижний)
    let lastRetryClickTimestamp = 0; // Временная метка (performance.now()) последнего клика для ТЕКУЩЕГО отслеживаемого сообщения

    // Порог времени в миллисекундах, прежде чем разрешить повторный клик на одном и том же сообщении
    const RETRY_CLICK_THRESHOLD_MS = 8000; // 8 секунд

    // Функция для более надежной проверки видимости элемента
    function isElementVisible(element) {
         if (!element) return false;

         // Быстрая проверка на отсоединенность от DOM или display: none у родителя
         if (element.offsetParent === null) return false;

         const style = getComputedStyle(element);
         // Проверка на display / visibility
         if (style.display === 'none' || style.visibility === 'hidden') return false;

         const rect = element.getBoundingClientRect();
         // Проверка на нулевые размеры или нахождение вне видимой части окна
         if (rect.width === 0 || rect.height === 0 || rect.right < 0 || rect.bottom < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight) return false;

         // Опционально: можно добавить проверку на перекрытие другими элементами, но это усложнит скрипт

         return true; // Считаем элемент видимым, если прошел все тесты
     }

    // Функция имитации клика
    function simulateClick(element) {
        if (!element) {
             console.error("simulateClick called with null element.");
             return;
        }
        const options = { bubbles: true, cancelable: true, view: window };

        try {
            const mousedownEvent = new MouseEvent('mousedown', options);
            element.dispatchEvent(mousedownEvent);

            const mouseupEvent = new MouseEvent('mouseup', options);
            element.dispatchEvent(mouseupEvent);

            const clickEvent = new MouseEvent('click', options);
            element.dispatchEvent(clickEvent);

             console.log('Simulated click event sequence on element:', element); // Лог об успешной имитации
        } catch (e) {
            console.error("Error simulating click:", e);
             console.log('Simulated click failed, falling back to native click.');
            element.click(); // Попытка нативного клика как запасной вариант
        }
    }


    function findRetryButton(messageElement) {
        console.log('Attempting to find Retry button near message element:', messageElement);
        let bestCandidate = null; // Кандидат с наивысшим приоритетом
        let bestPriority = -1; // Уровень приоритета лучшего кандидата (3 - уникальный SVG, 2 - ds-icon-button/button/role, 1 - ds-icon/pointer)
        let minDistance = Infinity; // Дистанция лучшего кандидата (для разрешения совпадений приоритетов)


        // --- Метод 1 (Приоритетный): Найти контейнер с уникальным SVG ID "重新生成" ---
        console.log('Method 1 (Prioritized): Attempting to find container with specific SVG ID "重新生成".');
        // Ищем элемент с уникальным ID SVG внутри области сообщения
        const specificSvgElement = messageElement.parentElement?.parentElement?.querySelector('#\u91CD\u65B0\u751F\u6210');

        if (specificSvgElement) {
             console.log('Method 1: Found specific SVG element with ID "重新生成".', specificSvgElement);
             // Теперь ищем ближайшего предка этого SVG, который является нашим контейнером кнопки/иконки
             let potentialButtonContainer = specificSvgElement.closest('button, a, [role="button"], div.ds-icon-button, div.ds-icon, span.ds-icon-button, span.ds-icon');

             if (potentialButtonContainer && isElementVisible(potentialButtonContainer)) {
                  console.log("Method 1: ---> Found a VISIBLE container for the specific SVG.", potentialButtonContainer);
                  // Найден контейнер, содержащий уникальный SVG, и он видим. Это наш лучший кандидат.
                  bestCandidate = potentialButtonContainer;
                  bestPriority = 3; // Самый высокий приоритет
                  console.log(`Method 1: Set BEST candidate based on containing specific SVG (Priority: ${bestPriority}).`, bestCandidate);
                  return bestCandidate; // Возвращаем сразу, т.к. это самый надежный способ найти кнопку
             } else if (potentialButtonContainer) {
                  console.log("Method 1: Found a container for the specific SVG, but it is NOT visible:", potentialButtonContainer);
             } else {
                  console.log("Method 1: Did not find a button/icon container as an ancestor of the specific SVG element.");
             }

        } else {
             console.log('Method 1: Did NOT find specific SVG element with ID "重新生成" near the message.');
        }


        // --- Метод 2 (Резервный): Поиск по классам / тексту + структурная/позиционная близость ---
        // Этот метод сработает, если Метод 1 не найдет SVG (например, если DOM изменится)
        console.log('Attempting Method 2 (Fallback: Proximity search with prioritization)...');
        let searchScope = messageElement.parentElement?.parentElement || document.body;
        // В Method 2 ищем элементы, которые могут быть кнопками (button, a, role=button) ИЛИ иконками
        const potentialButtonsAndIcons = searchScope.querySelectorAll('button, a, [role="button"], div[class*="icon"], span[class*="icon"]');

        console.log("Method 2: Starting proximity search among", potentialButtonsAndIcons.length, "potential elements...");
        potentialButtonsAndIcons.forEach(btn => { // Используем forEach для логирования всех кандидатов Method 2

             // *** Строгая проверка видимости для Method 2 ***
            if (!isElementVisible(btn)) {
                 // console.log("Method 2: Skipping non-visible element:", btn); // Опциональный лог
                 return; // Пропускаем невидимые в Method 2
            }

            const buttonText = btn.textContent.trim().toLowerCase();
            const btnHTML = btn.innerHTML; // Проверяем HTML на наличие уникального SVG ID
            const tag = btn.tagName.toLowerCase();
            const role = btn.getAttribute('role');
            const classes = btn.className;
            let cursorStyle = 'N/A';
            try { cursorStyle = getComputedStyle(btn).cursor; } catch(e) { /* ignore */ }

            // Определяем приоритет кандидата Method 2
            let currentPriority = 0;
            // Приоритет 3: Самый высокий - элемент содержит уникальный SVG ID
            if (btnHTML.includes('<rect id="\u91CD\u65B0\u751F\u6210"')) {
                 currentPriority = 3;
            }
            // Приоритет 2: Настоящие кнопки или элементы с явным классом кнопки/ролью (если не Приоритет 3)
            else if (tag === 'button' || role === 'button' || (typeof classes === 'string' && classes.includes('ds-icon-button'))) {
                 currentPriority = 2;
            }
            // Приоритет 1: Просто иконки, элементы с курсором pointer, или элементы с текстом (если не Приоритет 2 или 3)
            else if ((typeof classes === 'string' && classes.includes('ds-icon')) || ((tag === 'div' || tag === 'span') && cursorStyle === 'pointer') ||
                      buttonText.includes('повторить') || buttonText.includes('retry') || buttonText.includes('regenerate') || buttonText.includes(regenerateButtonText.toLowerCase())) {
                 currentPriority = 1;
            }


            // *** ДОБАВЛЕНО: Подробное логирование каждого кандидата Method 2 ***
            console.log(`  Method 2 Candidate: Tag: ${tag}, Class: "${classes}", Role: ${role}, Priority: ${currentPriority}, Text: "${buttonText}", Visible?: ${isElementVisible(btn)}, Cursor: ${cursorStyle}, Contains SVG: ${btnHTML.includes('<rect id="\u91CD\u65B0\u751F\u6210"')}`, btn);


            if (currentPriority > 0) { // Если это потенциальный кандидат для Method 2
                 try {
                    const messageRect = messageElement.getBoundingClientRect();
                    const buttonRect = btn.getBoundingClientRect();

                     // Расчет расстояния только для видимых потенциальных кандидатов
                     const distance = Math.abs(buttonRect.top - messageRect.bottom);


                     // Выбираем лучшего кандидата: сначала по приоритету, потом по близости
                     if (currentPriority > bestPriority) {
                         // Найден кандидат с более высоким приоритетом, чем текущий лучший (если такой уже был найден Метод 1 или ранее в Метод 2)
                         bestPriority = currentPriority;
                         minDistance = distance; // Обновляем мин. дистанцию для этого нового уровня приоритета
                         bestCandidate = btn; // Обновляем лучшего кандидата
                         console.log(`  Method 2: Found a NEW BEST candidate (Higher Priority: ${bestPriority}, Distance: ${distance}).`, bestCandidate);
                     } else if (currentPriority === bestPriority) {
                         // Найден кандидат с таким же приоритетом, как текущий лучший. Сравниваем дистанцию.
                          if (distance < minDistance) {
                              minDistance = distance;
                              bestCandidate = btn; // Обновляем лучшего кандидата (он ближе)
                              console.log(`  Method 2: Found a NEW BEST candidate (Same Priority: ${bestPriority}, Closer Distance: ${distance}).`, bestCandidate);
                          } else {
                               console.log(`  Method 2: Candidate (Priority: ${currentPriority}, Distance: ${distance}) is not better than current best (Priority: ${bestPriority}, Distance: ${minDistance}).`, btn);
                          }
                     } else {
                          console.log(`  Method 2: Candidate (Priority: ${currentPriority}) is lower than current best (Priority: ${bestPriority}).`, btn);
                     }

                 } catch (e) {
                     console.error("Method 2 (Fallback): Error getting bounding client rect:", e);
                 }
            } else if (currentPriority > 0 && !isElementVisible(btn)) {
                console.log("  Method 2: Candidate is not visible, skipping for selection.", btn);
            }
        }); // Конец forEach


        // --- Возвращаем лучшего кандидата, найденного любым методом ---
        if (bestCandidate && isElementVisible(bestCandidate)) { // Финальная проверка видимости перед возвратом
             console.log(`findRetryButton finished. Returning BEST visible candidate (Priority: ${bestPriority}).`, bestCandidate);
             return bestCandidate;
        }

        console.log('findRetryButton finished. No suitable visible button found.');
        return null;
    }


    function checkAndClick() {
        const paragraphs = document.querySelectorAll('p');
        let latestMessageElementThisCycle = null; // Переменная для последнего найденного элемента В ЭТОМ цикле

        // Итерируем по ВСЕМ параграфам, чтобы найти ПОСЛЕДНИЙ видимый с точным текстом
        for (const p of paragraphs) {
            if (p.textContent.trim() === busyMessageText && p.offsetParent !== null) {
                 if (isElementVisible(p)) {
                     latestMessageElementThisCycle = p; // Запоминаем последний найденный в этом цикле
                 }
            }
        }

        // --- Логика отслеживания состояния и клика ---
        if (latestMessageElementThisCycle) {
             // Найден (и выбран как самый нижний) элемент сообщения в этом цикле.

             // Проверяем, является ли этот самый нижний элемент НОВЫМ по сравнению с тем, который мы отслеживали.
            if (trackingMessageElement === null || !trackingMessageElement.isSameNode(latestMessageElementThisCycle)) {
                console.log('Detected a NEWEST "Server is busy" message instance (based on lowest visible exact match). Resetting state.');
                trackingMessageElement = latestMessageElementThisCycle; // Начинаем отслеживать НОВЫЙ самый нижний элемент
                lastRetryClickTimestamp = 0; // Сбрасываем временную метку для него
            }
             // else {
             //   console.log('Tracking the same latest "Server is busy" message instance.');
             // }

            // Теперь ищем и кликаем кнопку, связанную с ОТСЛЕЖИВАЕМЫМ элементом
            const retryButtonElement = findRetryButton(trackingMessageElement); // Ищем кнопку ОТНОСИТЕЛЬНО отслеживаемого элемента

            // --- Логирование клика (оставляем основные логи) ---
            if (retryButtonElement) {
                const now = performance.now();
                // Проверяем временной порог ТОЛЬКО для отслеживаемого элемента
                if (lastRetryClickTimestamp === 0 || now - lastRetryClickTimestamp > RETRY_CLICK_THRESHOLD_MS) {
                    console.log('Potential retry button found for tracked message. Attempting to click.');
                    // Логи, которые показывают элемент перед кликом
                    console.log('Element being clicked:', retryButtonElement);
                    console.log('Element tag:', retryButtonElement.tagName.toLowerCase());
                    if (retryButtonElement.tagName.toLowerCase() === 'a') {
                         console.log('Element href:', retryButtonElement.href);
                    }

                    // *** ИСПОЛЬЗУЕМ ФУНКЦИЮ ИМИТАЦИИ КЛИКА ***
                    simulateClick(retryButtonElement);

                    lastRetryClickTimestamp = now;
                    console.log('Clicked. Updated lastRetryClickTimestamp.');
                } else {
                     // console.log(`Potential retry button found, but within threshold (${(now - lastRetryClickTimestamp).toFixed(0)}ms since last click). Waiting.`);
                }
            } else {
                 console.log('Tracked message present, but retry button not found.');
            }

        } else {
            // В этом цикле НЕ найдено ни одного элемента с точным текстом "Server is busy".
            if (trackingMessageElement !== null) {
                console.log('"Server is busy" message (previously tracked) no longer found. Resetting state.');
                trackingMessageElement = null; // Перестаем отслеживать старый элемент
                lastRetryClickTimestamp = 0;
            }
             // else {
             //    console.log('No "Server is busy" message to track.');
             // }
        }
    }

    const checkInterval = setInterval(checkAndClick, 2000);

    console.log('Tampermonkey script "Авто-перепопытка при Server busy" запущен. Ожидание самого нижнего сообщения о занятости и доступной кнопки...');

})();