Grok Auto-Retry (Persistent UI)

Fast retry. When limit is reached, it restores text but stops clicking (prevents crash).

当前为 2025-12-05 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Grok Auto-Retry (Persistent UI)
// @namespace    http://tampermonkey.net/
// @version      7.5
// @description  Fast retry. When limit is reached, it restores text but stops clicking (prevents crash).
// @author       You
// @license MIT
// @match        https://grok.com/*
// @match        https://*.grok.com/*
// @match        https://grok.x.ai/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const TARGET_TEXTAREA_SELECTOR = 'textarea[aria-label="Make a video"]';
    const RETRY_BUTTON_SELECTOR = 'button[aria-label="Make video"]';
    const MODERATION_TEXT = "Content Moderated. Try a different idea.";

    // FAST SETTINGS (Restored as requested)
    const RETRY_DELAY_MS = 1000; // 1 second wait before clicking
    const COOLDOWN_MS = 2000;    // Short cooldown to prevent double-clicks

    // --- LOAD SAVED SETTINGS ---
    let maxRetries = GM_getValue('maxRetries', 5);
    let uiToggleKey = GM_getValue('uiToggleKey', 'h');
    let autoClickEnabled = GM_getValue('autoClickEnabled', true);
    let isUiVisible = GM_getValue('isUiVisible', true);

    let isRetryEnabled = true;

    // --- STATE VARIABLES ---
    let lastTypedPrompt = "";
    let lastRetryTimestamp = 0;
    let currentRetryCount = 0;

    // CRITICAL FLAG: Stops the crash loop
    let limitReached = false;

    // --- STYLES ---
    GM_addStyle(`
        #grok-control-panel {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 250px;
            background-color: #15202b;
            border: 1px solid #38444d;
            border-radius: 12px;
            padding: 15px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            color: white;
            z-index: 99999;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            transition: opacity 0.3s ease;
        }
        #grok-control-panel.hidden {
            opacity: 0;
            pointer-events: none;
        }
        .grok-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }
        .grok-title {
            font-size: 14px;
            font-weight: bold;
            color: #fff;
        }
        .grok-toggle-btn {
            background: #00ba7c;
            border: none;
            color: white;
            padding: 4px 10px;
            border-radius: 15px;
            font-size: 11px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.2s;
        }
        .grok-toggle-btn.off {
            background: #f4212e;
        }
        .grok-input-group {
            display: flex;
            align-items: center;
            font-size: 12px;
            color: #8b98a5;
        }
        .grok-checkbox-group {
            display: flex;
            align-items: center;
            font-size: 12px;
            color: #fff;
            margin-bottom: 8px;
            cursor: pointer;
        }
        .grok-checkbox-group input {
            margin-right: 8px;
            cursor: pointer;
        }
        #grok-retry-limit {
            width: 50px;
            background: #273340;
            border: 1px solid #38444d;
            color: white;
            border-radius: 4px;
            padding: 4px;
            margin-left: 8px;
            text-align: center;
        }
        #grok-status-text {
            font-size: 11px;
            margin-top: 5px;
            padding-top: 5px;
            border-top: 1px solid #38444d;
            color: #00ba7c;
            text-align: center;
        }
        #grok-footer-wrapper {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 6px;
            font-size: 10px;
            color: #555;
            cursor: pointer;
            user-select: none;
            transition: color 0.2s;
        }
        #grok-footer-wrapper:hover {
            color: #8b98a5;
        }
        .footer-arrow { margin: 0 5px; font-size: 12px; }
        .status-active { color: #ffd400 !important; }
        .status-error { color: #f4212e !important; }
    `);

    // --- CREATE UI PANEL ---
    const panel = document.createElement('div');
    panel.id = 'grok-control-panel';

    if (!isUiVisible) {
        panel.classList.add('hidden');
    }

    panel.innerHTML = `
        <div class="grok-row">
            <span class="grok-title">Fast Auto-Retry</span>
            <button id="grok-toggle-btn" class="grok-toggle-btn">ON</button>
        </div>

        <label class="grok-checkbox-group" title="Uncheck to only restore text without clicking retry">
            <input type="checkbox" id="grok-autoclick-cb" ${autoClickEnabled ? 'checked' : ''}>
            Auto-Click Button
        </label>

        <div class="grok-row">
            <div class="grok-input-group">
                <label for="grok-retry-limit">Max Retries:</label>
                <input type="number" id="grok-retry-limit" value="${maxRetries}" min="1" max="99">
            </div>
        </div>
        <div id="grok-status-text">Ready</div>

        <div id="grok-footer-wrapper" title="Click to change shortcut">
            <span>Change Hide Key</span>
            <span class="footer-arrow">&rarr;</span>
            <span id="grok-keybind-display">Alt+${uiToggleKey.toUpperCase()}</span>
        </div>
    `;
    document.body.appendChild(panel);

    // --- UI REFERENCES ---
    const toggleBtn = document.getElementById('grok-toggle-btn');
    const autoClickCb = document.getElementById('grok-autoclick-cb');
    const limitInput = document.getElementById('grok-retry-limit');
    const statusText = document.getElementById('grok-status-text');
    const footerWrapper = document.getElementById('grok-footer-wrapper');
    const keybindDisplay = document.getElementById('grok-keybind-display');

    // --- EVENT LISTENERS ---

    // 1. Master Toggle ON/OFF
    toggleBtn.addEventListener('click', () => {
        isRetryEnabled = !isRetryEnabled;
        if (isRetryEnabled) {
            toggleBtn.textContent = "ON";
            toggleBtn.classList.remove('off');
            limitReached = false; // Reset lock
            currentRetryCount = 0;
            updateStatus("Ready (Resumed)");
        } else {
            toggleBtn.textContent = "OFF";
            toggleBtn.classList.add('off');
            updateStatus("Script Disabled", "error");
        }
    });

    // 2. Auto-Click Checkbox
    autoClickCb.addEventListener('change', (e) => {
        autoClickEnabled = e.target.checked;
        GM_setValue('autoClickEnabled', autoClickEnabled);
        updateStatus(autoClickEnabled ? "Auto-Click Enabled" : "Restore Only Mode");
    });

    // 3. Limit Input
    limitInput.addEventListener('change', (e) => {
        let val = parseInt(e.target.value, 10);
        if (val < 1) val = 1;
        maxRetries = val;
        GM_setValue('maxRetries', maxRetries);
        updateStatus(`Limit set to ${maxRetries}`);
    });

    // 4. Change Keybind
    footerWrapper.addEventListener('click', () => {
        const originalText = keybindDisplay.textContent;
        keybindDisplay.textContent = "Press key...";
        footerWrapper.style.color = "#ffd400";

        function onKeyCapture(e) {
            e.preventDefault();
            e.stopPropagation();
            if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return;
            const newKey = e.key.toLowerCase();
            if (newKey.length === 1) {
                uiToggleKey = newKey;
                GM_setValue('uiToggleKey', uiToggleKey);
                keybindDisplay.textContent = `Alt+${uiToggleKey.toUpperCase()}`;
            } else {
                keybindDisplay.textContent = originalText;
            }
            footerWrapper.style.color = "";
            document.removeEventListener('keydown', onKeyCapture, true);
        }
        document.addEventListener('keydown', onKeyCapture, true);
    });

    // 5. Hide/Show UI Hotkey
    document.addEventListener('keydown', (e) => {
        if (e.altKey && e.key.toLowerCase() === uiToggleKey) {
            e.preventDefault();
            isUiVisible = !isUiVisible;
            GM_setValue('isUiVisible', isUiVisible);
            panel.classList.toggle('hidden', !isUiVisible);
        }
    });

    // --- HELPER: UPDATE STATUS ---
    function updateStatus(msg, type = 'normal') {
        statusText.textContent = msg;
        statusText.className = '';
        if (type === 'active') statusText.classList.add('status-active');
        if (type === 'error') statusText.classList.add('status-error');
        
        if (msg.startsWith("Ready")) {
             statusText.textContent = autoClickEnabled
                 ? `Ready (${currentRetryCount} / ${maxRetries})`
                 : `Ready (Restore Only)`;
        }
    }

    // --- CORE: CAPTURE PROMPT ---
    document.addEventListener('input', (e) => {
        if (e.target.matches(TARGET_TEXTAREA_SELECTOR)) {
            const val = e.target.value;
            // When user types, we assume it's a new prompt.
            // WE MUST RESET THE LIMIT LOCK HERE so the script works for the new prompt.
            if (val && val.trim().length > 0) {
                if (val !== lastTypedPrompt) {
                    currentRetryCount = 0;
                    limitReached = false; // <--- UNLOCK
                    updateStatus(`Ready`);
                }
                lastTypedPrompt = val;
            }
        }
    }, true);

    // --- CORE: MONITOR ---
    const observer = new MutationObserver(() => {
        if (!isRetryEnabled) return;
        
        // CRITICAL: If limit was reached, DO NOT SCAN DOM. 
        // This effectively "turns off" the script until you type something new.
        if (limitReached) return;

        const allSpans = Array.from(document.querySelectorAll('span'));
        const modSpan = allSpans.find(s => s.textContent.trim() === MODERATION_TEXT);

        if (modSpan) {
            const now = Date.now();
            if (now - lastRetryTimestamp < COOLDOWN_MS) return;
            handleRestore(now);
        }
    });

    observer.observe(document.body, { childList: true, subtree: true, characterData: true });

    // --- CORE: RESTORE & OPTIONAL CLICK ---
    function handleRestore(now) {
        if (!lastTypedPrompt) return;

        lastRetryTimestamp = now;

        const textarea = document.querySelector(TARGET_TEXTAREA_SELECTOR);
        const button = document.querySelector(RETRY_BUTTON_SELECTOR);

        if (textarea) {
            // 1. ALWAYS RESTORE THE TEXT FIRST
            const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
            nativeTextAreaValueSetter.call(textarea, lastTypedPrompt);
            textarea.dispatchEvent(new Event('input', { bubbles: true }));

            // 2. CHECK IF WE HIT THE LIMIT
            // If we are AT or ABOVE the max retries, we DO NOT CLICK.
            if (autoClickEnabled && currentRetryCount >= maxRetries) {
                updateStatus(`Limit Reached (Text Restored)`, "error");
                
                // CRITICAL: Set this flag. The MutationObserver will now ignore the page.
                // This prevents the infinite loop crash.
                limitReached = true; 
                return; 
            }

            // 3. IF LIMIT NOT REACHED, CLICK BUTTON
            if (autoClickEnabled && button) {
                currentRetryCount++; // Increment count
                
                updateStatus(`Retrying... (${currentRetryCount} / ${maxRetries})`, "active");
                
                setTimeout(() => {
                    button.click();
                }, RETRY_DELAY_MS);
            } else {
                updateStatus("Text Restored (Waiting)", "active");
                setTimeout(() => updateStatus("Ready"), 2000);
            }
        }
    }

})();