NovelAI 计数连续生成

更加纯粹。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         NovelAI 计数连续生成
// @namespace    https://novelai.net
// @version      1.5
// @description  更加纯粹。
// @author       Tako
// @match        https://novelai.net/image
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @icon         https://www.google.com/s2/favicons?sz=64&domain=novelai.net
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let loopDelay, loopJitter, imagesToRun, confirmAnlas;
    let isAutoRunning = false;
    let imagesProcessed = 0;

    const anlasXpath = "/html/body/div[2]/div[2]/div[3]/div[3]/div[1]/div[1]/div[5]/button/div/div[1]/span";
    const seedXpath = "/html/body/div[2]/div[2]/div[3]/div[3]/div[1]/div[1]/div[5]/div/div/div/div[3]/button/span";

    let uiContainer, startButton, statusDisplay, hoverTrigger;

    function loadSettings() {
        loopDelay = GM_getValue('nai_ac_loopDelay', 5000);
        loopJitter = GM_getValue('nai_ac_loopJitter', 2000);
        imagesToRun = GM_getValue('nai_ac_imagesToRun', Infinity);
        confirmAnlas = GM_getValue('nai_ac_confirmAnlas', false);
    }

    function saveSettings() {
        GM_setValue('nai_ac_loopDelay', loopDelay);
        GM_setValue('nai_ac_loopJitter', loopJitter);
        GM_setValue('nai_ac_imagesToRun', imagesToRun);
        GM_setValue('nai_ac_confirmAnlas', confirmAnlas);
    }

    function findGenerateButton() {
        const xpath = "//button[span[starts-with(text(), 'Generate') and contains(text(), 'Image')]]";
        let result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        return result.singleNodeValue;
    }

    async function waitForGeneration() {
        return new Promise((resolve, reject) => {
            const timeout = 30000;
            const interval = 500;
            let elapsed = 0;
            const id = setInterval(() => {
                elapsed += interval;
                const generateBtn = findGenerateButton();
                if (generateBtn && !generateBtn.disabled) {
                    clearInterval(id);
                    resolve();
                    return;
                }
                if (elapsed >= timeout) {
                    clearInterval(id);
                    reject(new Error(`生成超时 (${timeout / 1000}秒)`));
                }
            }, interval);
        });
    }

    function resetSeed() {
        try {
            let seedSpan = document.evaluate(seedXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            if (seedSpan) {
                let event = new MouseEvent("click", { bubbles: true, cancelable: true });
                seedSpan.dispatchEvent(event);
                console.log("Seed已重置");
            }
        } catch (e) { console.warn("重置Seed失败", e); }
    }

    function checkAnlas() {
        return new Promise((resolve) => {
            try {
                let anlasNode = document.evaluate(anlasXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
                if (anlasNode && parseInt(anlasNode.innerText, 10) > 0) {
                    if (confirm("你要消耗Anlas进行连点吗?")) resolve(true);
                    else resolve(false);
                } else { resolve(true); }
            } catch (e) {
                console.warn("未检测到Anlas消耗,默认允许执行。");
                resolve(true);
            }
        });
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }


    async function startAutomation() {
        if (isAutoRunning) return;
        if (confirmAnlas && !(await checkAnlas())) {
            updateStatus("用户取消");
            return;
        }

        isAutoRunning = true;
        imagesProcessed = 0;
        uiContainer.classList.add('running');
        startButton.innerText = "停止";

        const initialBtn = findGenerateButton();
        if (!initialBtn) {
             updateStatus("错误: 未找到按钮", 'error');
             stopAutomation();
             return;
        }

        if (initialBtn.disabled) {
            updateStatus("等待当前生成...");
            try {
                await waitForGeneration(); // 等待当前正在进行的生成
                resetSeed(); // 重置 seed 准备开始循环
            } catch (error) {
                console.error('连点器启动时等待失败:', error);
                updateStatus(`错误: ${error.message}`, 'error');
                stopAutomation();
                return;
            }
        }

        updateStatus("启动中...");

        for (let i = 1; (i <= imagesToRun || imagesToRun === Infinity); i++) {
            if (!isAutoRunning) {
                updateStatus("已停止");
                break;
            }

            imagesProcessed = i;
            let countDisplay = imagesToRun === Infinity ? '∞' : imagesToRun;
            updateStatus(`生成中: ${i}/${countDisplay}`);

            try {
                let generateButton = findGenerateButton();
                if (!generateButton) throw new Error('生成按钮消失');

                if (generateButton.disabled) {
                    updateStatus("按钮占用, 等待中...");
                    await waitForGeneration();
                    generateButton = findGenerateButton(); // 重新获取
                    if (!generateButton || generateButton.disabled) {
                        throw new Error('按钮持续占用');
                    }
                }
                generateButton.click();
                await waitForGeneration();
                resetSeed();

                if (i < imagesToRun || imagesToRun === Infinity) {
                    let randomJitter = (Math.random() * 2 - 1) * loopJitter;
                    let waitTime = Math.max(500, loopDelay + randomJitter);
                    let waited = 0;
                    while (waited < waitTime && isAutoRunning) {
                        updateStatus(`等待: (${(waited / 1000).toFixed(1)}s / ${(waitTime / 1000).toFixed(1)}s)`);
                        await sleep(250);
                        waited += 250;
                    }
                }
            } catch (error) {
                console.error('连点器生成失败:', error);
                updateStatus(`错误: ${error.message}`, 'error');
                stopAutomation();
                break;
            }
        }

        if (isAutoRunning) {
            stopAutomation();
            updateStatus(`完成: ${imagesProcessed} 张`);
        }
    }

    function stopAutomation() {
        isAutoRunning = false;
        uiContainer.classList.remove('running');
        startButton.innerText = "启动";
        if (!statusDisplay.innerText.includes("错误")) {
            updateStatus("已停止");
        }
    }

    function toggleAutomation() {
        if (isAutoRunning) {
            stopAutomation();
        } else {
            try {
                loopDelay = parseFloat(document.getElementById('nai-ac-delay').value) * 1000 || 5000;
                loopJitter = parseFloat(document.getElementById('nai-ac-jitter').value) * 1000 || 2000;
                let imagesInput = parseInt(document.getElementById('nai-ac-images').value, 10);
                imagesToRun = (!isNaN(imagesInput) && imagesInput > 0) ? imagesInput : Infinity;
                confirmAnlas = document.getElementById('nai-ac-confirm').checked;

                saveSettings();

            } catch(e) { console.error("读取设置失败", e); }
            startAutomation();
        }
    }

    function updateStatus(text, type = 'info') {
        if (statusDisplay) {
            statusDisplay.innerText = text;
            statusDisplay.style.color = type === 'error' ? '#FF6B6B' : 'rgb(245, 243, 194)';
        }
    }

    function createUI() {
        uiContainer = document.createElement('div');
        uiContainer.id = 'nai-auto-clicker-container';

        hoverTrigger = document.createElement('div');
        hoverTrigger.id = 'nai-ac-hover-trigger';
        hoverTrigger.innerHTML = '▶';
        uiContainer.appendChild(hoverTrigger);

        const menu = document.createElement('div');
        menu.id = 'nai-ac-menu';
        statusDisplay = document.createElement('div');
        statusDisplay.id = 'nai-ac-status';
        statusDisplay.innerText = "就绪";
        menu.appendChild(statusDisplay);

        menu.appendChild(createInputGroup('nai-ac-delay', '延迟 (s):', (loopDelay / 1000)));
        menu.appendChild(createInputGroup('nai-ac-jitter', '抖动 (±s):', (loopJitter / 1000)));
        menu.appendChild(createInputGroup('nai-ac-images', '张数:', imagesToRun === Infinity ? '' : imagesToRun, '留空无限'));
        menu.appendChild(createCheckboxGroup('nai-ac-confirm', '消耗前确认', confirmAnlas));

        startButton = document.createElement('button');
        startButton.id = 'nai-ac-start-btn';
        startButton.innerText = '启动';
        startButton.onclick = toggleAutomation;
        menu.appendChild(startButton);
        uiContainer.appendChild(menu);

        document.body.appendChild(uiContainer);

        let initX, containerX;
        hoverTrigger.addEventListener("mousedown", function (e) {
            e.preventDefault();
            initX = e.pageX;
            containerX = uiContainer.offsetLeft;
            if (e.target !== hoverTrigger) return;
            hoverTrigger.classList.add('grabbing');
            document.addEventListener("mousemove", mouseMoveHandler);
            document.addEventListener("mouseup", mouseUpHandler);
        });
        function mouseMoveHandler(e) {
            let moveX = e.pageX - initX;
            let newLeft = containerX + moveX;
            if (newLeft < 0) newLeft = 0;
            if (newLeft > window.innerWidth - uiContainer.offsetWidth) {
                newLeft = window.innerWidth - uiContainer.offsetWidth;
            }
            uiContainer.style.left = newLeft + "px";
            uiContainer.style.right = 'auto';
        }
        function mouseUpHandler() {
            hoverTrigger.classList.remove('grabbing');
            document.removeEventListener("mousemove", mouseMoveHandler);
            document.removeEventListener("mouseup", mouseUpHandler);
        }

        let hideMenuTimer;
        uiContainer.addEventListener('mouseenter', () => {
            clearTimeout(hideMenuTimer);
            menu.style.display = 'block';
        });
        uiContainer.addEventListener('mouseleave', () => {
            hideMenuTimer = setTimeout(() => {
                menu.style.display = 'none';
            }, 200);
        });
    }

    function createInputGroup(id, labelText, defaultValue, placeholder = '') {
        const group = document.createElement('div');
        group.className = 'nai-ac-input-group';
        const label = document.createElement('label');
        label.setAttribute('for', id);
        label.innerText = labelText;
        group.appendChild(label);
        const input = document.createElement('input');
        input.type = 'number';
        input.id = id;
        input.value = defaultValue;
        input.placeholder = placeholder;
        group.appendChild(input);
        return group;
    }

    function createCheckboxGroup(id, labelText, isChecked) {
        const group = document.createElement('div');
        group.className = 'nai-ac-input-group nai-ac-checkbox-group';
        const label = document.createElement('label');
        label.setAttribute('for', id);
        label.innerText = labelText;
        label.style.cursor = 'pointer';
        group.appendChild(label);
        const input = document.createElement('input');
        input.type = 'checkbox';
        input.id = id;
        input.checked = isChecked;
        group.appendChild(input);
        return group;
    }


    function addStyles() {
        GM_addStyle(`
            @keyframes nai-ac-spin-border {
                0% { border-color: rgb(245, 243, 194) transparent transparent transparent; }
                25% { border-color: transparent rgb(245, 243, 194) transparent transparent; }
                50% { border-color: transparent transparent rgb(245, 243, 194) transparent; }
                75% { border-color: transparent transparent transparent rgb(245, 243, 194); }
                100% { border-color: rgb(245, 243, 194) transparent transparent transparent; }
            }

            #nai-auto-clicker-container {
                position: fixed;
                bottom: 0;
                left: 30%;
                z-index: 9999;
                font-family: 'Source Sans Pro', sans-serif;
                user-select: none;
            }

            #nai-ac-hover-trigger {
                width: 42px; height: 42px;
                background-color: rgb(34, 37, 63);
                border: 2px solid #555;
                border-bottom: none;
                border-radius: 8px 8px 0 0;
                color: rgb(245, 243, 194);
                display: flex; align-items: center; justify-content: center;
                cursor: grab;
                font-size: 15px; box-sizing: border-box;
            }

            #nai-ac-hover-trigger.grabbing {
                cursor: grabbing;
            }

            #nai-auto-clicker-container.running #nai-ac-hover-trigger {
                border-width: 2px; border-style: solid;
                animation: nai-ac-spin-border 1.2s linear infinite;
            }

            #nai-ac-menu {
                display: none;
                position: absolute;
                bottom: 40px;
                left: 50%;
                transform: translateX(-50%);
                width: 180px; padding: 12px;
                background-color: rgb(34, 37, 63);
                border: 1px solid #555; border-radius: 8px;
                box-shadow: -5px -5px 15px rgba(0,0,0,0.3);
            }

            /* 移除: CSS HOVER 规则,改用 JS 控制 */
            /*
            #nai-auto-clicker-container:hover #nai-ac-menu {
                 display: block;
                 left: 50%;
                 transform: translateX(-50%);
            }
            */

            #nai-ac-status {
                color: rgb(245, 243, 194); font-size: 14px;
                text-align: center; margin-bottom: 10px;
                border-bottom: 1px solid #555; padding-bottom: 8px;
                min-height: 1.2em;
            }

            .nai-ac-input-group {
                display: flex; justify-content: space-between;
                align-items: center; margin-bottom: 8px;
            }
            .nai-ac-input-group label {
                color: #DDD; font-size: 13px; margin-right: 5px;
            }
            .nai-ac-input-group input {
                width: 80px; background-color: rgb(20, 22, 40);
                color: white; border: 1px solid #555;
                border-radius: 4px; padding: 4px 6px; font-size: 13px;
                -moz-appearance: textfield;
            }
            .nai-ac-input-group input::-webkit-inner-spin-button,
            .nai-ac-input-group input::-webkit-outer-spin-button {
                -webkit-appearance: none;
            }

            .nai-ac-checkbox-group input[type="checkbox"] {
                width: 16px; height: 16px; margin: 0;
                padding: 0; cursor: pointer;
                accent-color: rgb(111, 128, 255);
            }

            #nai-ac-start-btn {
                width: 100%; padding: 8px; margin-top: 5px;
                background-color: rgb(111, 128, 255); color: white;
                border: none; border-radius: 4px; cursor: pointer;
                font-weight: bold; font-size: 14px;
            }
            #nai-ac-start-btn:hover { background-color: rgb(130, 145, 255); }
            #nai-auto-clicker-container.running #nai-ac-start-btn {
                 background-color: #FF6B6B;
            }
        `);
    }

    loadSettings();
    addStyles();
    createUI();

})();