您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
从本地TXT文件读取提示词,全自动批量生成图像。支持顺序/随机模式,可自动记忆,生成失败自动重试。优化版:添加失焦折叠、默认折叠、性能优化
// ==UserScript== // @name NovelAI 批量Roll图助手 // @namespace http://tampermonkey.net/ // @version 1.9 // @description 从本地TXT文件读取提示词,全自动批量生成图像。支持顺序/随机模式,可自动记忆,生成失败自动重试。优化版:添加失焦折叠、默认折叠、性能优化 // @author Takoro // @match https://novelai.net/image // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; // ========== 全局变量 ========== let promptsArray = []; let filteredPromptsArray = []; let currentPromptIndex = 0; let isAutoRunning = false; let promptsProcessedThisRun = 0; let titleElement; let panelElement; // DOM元素缓存 const domCache = { generateButton: null, promptEditableDiv: null, lastCacheTime: 0, cacheTimeout: 5000 // 5秒缓存过期 }; const STORAGE_KEYS = { START_INDEX: 'nai_helper_start_index', PROMPT_TEMPLATE: 'nai_helper_prompt_template', CHARACTER_PROMPT_TEMPLATE: 'nai_helper_char_template', LAST_FILE_NAME: 'nai_helper_last_file_name', LAST_FILE_CONTENT: 'nai_helper_last_file_content', RANDOM_MODE: 'nai_helper_random_mode', RANDOM_COUNT: 'nai_helper_random_count', FILTER_KEYWORD: 'nai_helper_filter_keyword', PANEL_COLLAPSED: 'nai_helper_panel_collapsed' }; // ========== 工具函数 ========== const sleep = ms => new Promise(res => setTimeout(res, ms)); function getElementByXPath(path) { return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } async function waitForElement(selector, parent = document, timeout = 5000) { return new Promise((resolve) => { const interval = 100; let elapsed = 0; const timer = setInterval(() => { const element = parent.querySelector(selector); if (element) { clearInterval(timer); resolve(element); } elapsed += interval; if (elapsed >= timeout) { clearInterval(timer); resolve(null); } }, interval); }); } function adjustTextareaHeight(el) { if (!el) return; el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; } // 清除DOM缓存 function clearDomCache() { domCache.generateButton = null; domCache.promptEditableDiv = null; domCache.lastCacheTime = 0; } // ========== 面板折叠相关(优化版 - 低性能开销)========== function togglePanel(forceState = null) { if (!panelElement) return; const shouldCollapse = forceState !== null ? forceState : !panelElement.classList.contains('collapsed'); if (shouldCollapse) { panelElement.classList.add('collapsed'); GM_setValue(STORAGE_KEYS.PANEL_COLLAPSED, true); } else { panelElement.classList.remove('collapsed'); GM_setValue(STORAGE_KEYS.PANEL_COLLAPSED, false); } } // 优化的失焦折叠 - 降低灵敏度,减少性能开销 let collapseTimeout = null; let lastInteractionTime = 0; function setupClickOutsideHandler() { // 使用单一的点击监听器,性能开销最小 document.addEventListener('mousedown', (e) => { if (!panelElement) return; // 如果面板已折叠,不处理 if (panelElement.classList.contains('collapsed')) return; // 如果点击的是面板内部,清除定时器并记录交互时间 if (panelElement.contains(e.target)) { if (collapseTimeout) { clearTimeout(collapseTimeout); collapseTimeout = null; } lastInteractionTime = Date.now(); return; } // 如果刚刚与面板交互过(800ms内),不折叠 const timeSinceLastInteraction = Date.now() - lastInteractionTime; if (timeSinceLastInteraction < 800) { return; } // 清除之前的定时器 if (collapseTimeout) { clearTimeout(collapseTimeout); } // 延迟800ms后折叠,给用户充足的反应时间 collapseTimeout = setTimeout(() => { // 再次检查是否刚交互过 if (Date.now() - lastInteractionTime >= 800) { togglePanel(true); } collapseTimeout = null; }, 800); }, { passive: true }); // 使用passive提升性能 } // ========== UI创建 ========== function createUIPanel() { const panel = document.createElement('div'); panel.id = 'nai-prompt-helper-panel'; panel.innerHTML = ` <h3>提示词批量助手</h3> <div class="content-wrapper"> <div class="file-controls"> <label for="prompt-file-input" class="custom-file-button" title="选择新的 .txt 文件"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20px" height="20px"><path d="M14 2H6C4.9 2 4.01 2.9 4.01 4L4 20C4 21.1 4.89 22 5.99 22H18C19.1 22 20 21.1 20 20V8L14 2ZM18 20H6V4H13V9H18V20Z"></path></svg> </label> <input type="file" id="prompt-file-input" accept=".txt" style="display: none;"> <div id="file-info-display">当前未加载文件</div> </div> <div class="control-group"> <label for="preset-prompt-input">1. 提示词模板</label> <textarea id="preset-prompt-input" rows="1" placeholder="脚本会从本地txt选择一组Prompt来替换该输入框中的 [替换词] 后填入NAI的主提示词框。\n写法:\nartist1, artist2, [替换词], very aesthetic, masterpiece, no text"></textarea> </div> <div class="control-group"> <label for="character-prompt-input">2. 角色模板</label> <textarea id="character-prompt-input" rows="1" placeholder="以主动换行为一条,随机抽取,留空则会清空页面上的角色框内容。"></textarea> </div> <div class="control-group filter-controls"> <label id="filter-label" for="filter-keyword-input">3. 从文件中筛选 (未启用)</label> <div class="input-with-button"> <input type="text" id="filter-keyword-input" placeholder="输入关键词筛选"> <button id="apply-filter-btn">应用</button> </div> </div> <hr> <div class="heading-with-control"> <label>4. 自动化设置</label> <div class="toggle-switch-wrapper"> <label class="toggle-switch-label" for="random-mode-checkbox">随机模式</label> <label class="toggle-switch"> <input type="checkbox" id="random-mode-checkbox"> <span class="slider"></span> </label> </div> </div> <div class="control-group settings"> <div> <label for="start-index-input" id="start-index-label">文件行数:</label> <input type="number" id="start-index-input" value="1" min="1"> </div> <div> <label for="images-per-prompt-input">每条张数:</label> <input type="number" id="images-per-prompt-input" value="1" min="1"> </div> <div> <label for="delay-input">延迟(秒):</label> <input type="number" id="delay-input" value="2" min="0" step="0.5"> </div> <div> <label for="max-retries-input">重试次数:</label> <input type="number" id="max-retries-input" value="5" min="0"> </div> </div> <hr> <button id="main-control-btn">▶️ 开始执行</button> <div id="status-display">状态:等待操作...</div> <div id="run-counter-display">本次运行: 0 条</div> </div>`; document.body.appendChild(panel); panelElement = panel; titleElement = panel.querySelector('h3'); addEventListeners(); loadSettings(); setupClickOutsideHandler(); // 设置初始折叠状态(默认折叠) const savedCollapsedState = GM_getValue(STORAGE_KEYS.PANEL_COLLAPSED, true); togglePanel(savedCollapsedState); } // ========== NAI页面元素查找(优化版)========== function removeUnwantedElement() { const xpath = '//*[@id="__next"]/div[2]/div[3]/div[3]/div/div[1]/div[5]/div[2]'; const interval = 3000; let attempts = 0; const maxAttempts = 3; const checker = setInterval(() => { attempts++; const element = getElementByXPath(xpath); if (element) { element.remove(); clearInterval(checker); } if (attempts >= maxAttempts) { clearInterval(checker); } }, interval); } async function switchToCharacterPromptTab() { let promptButton = Array.from(document.querySelectorAll('button')) .find(btn => btn.textContent.trim() === 'Prompt'); if (!promptButton) { try { const xpath = '//*[@id="__next"]/div[2]/div[3]/div[3]/div/div[1]/div[3]/div[2]/div/div[3]/div[3]/div[2]/div[1]/div[1]/div[1]/button'; promptButton = getElementByXPath(xpath); } catch (e) { console.error('切换到Prompt标签失败:', e); } } if (promptButton) { promptButton.click(); await sleep(200); return true; } return false; } async function addCharacterSlot() { const addCharButton = Array.from(document.querySelectorAll('button')) .find(btn => btn.textContent.trim() === 'Add Character'); if (!addCharButton) return false; addCharButton.click(); const popperMenu = await waitForElement('div[data-popper-placement]'); if (!popperMenu) return false; let otherButton = Array.from(popperMenu.querySelectorAll('button')) .find(b => b.textContent.trim().toLowerCase().includes('other')); if (!otherButton) { try { otherButton = getElementByXPath('/html/body/div[11]/button[3]'); } catch (e) { return false; } } if (!otherButton) return false; otherButton.click(); await sleep(300); return true; } async function handleCharacterPrompt() { const charTemplate = document.getElementById('character-prompt-input').value.trim(); const addCharButton = Array.from(document.querySelectorAll('button')) .find(btn => btn.textContent.trim() === 'Add Character'); if (!addCharButton) return true; const characterInputs = document.querySelectorAll('[class*="character-prompt-input"] [contenteditable="true"] p'); if (!charTemplate) { if (characterInputs.length > 0) { characterInputs.forEach(p => { if (p.textContent) { p.textContent = ''; p.dispatchEvent(new Event('input', { bubbles: true })); } }); } return true; } await switchToCharacterPromptTab(); const charPrompts = charTemplate.split(/\r?\n/).filter(line => line.trim() !== ''); if (charPrompts.length === 0) return true; const randomCharPrompt = charPrompts[Math.floor(Math.random() * charPrompts.length)]; let promptElement = document.querySelector('[class*="character-prompt-input-1"] [contenteditable="true"] p'); if (!promptElement) { if (!(await addCharacterSlot())) return false; promptElement = await waitForElement('[class*="character-prompt-input-1"] [contenteditable="true"] p'); if (!promptElement) return false; } if (promptElement.textContent !== randomCharPrompt) { promptElement.textContent = randomCharPrompt; promptElement.dispatchEvent(new Event('input', { bubbles: true })); } return true; } function findPromptEditableDiv() { const now = Date.now(); if (domCache.promptEditableDiv && (now - domCache.lastCacheTime < domCache.cacheTimeout)) { if (document.contains(domCache.promptEditableDiv)) { return domCache.promptEditableDiv; } } const element = document.querySelector('[class*="prompt-input-box-prompt"] [contenteditable="true"], [class*="prompt-input-box-base-prompt"] [contenteditable="true"]'); if (element) { domCache.promptEditableDiv = element; domCache.lastCacheTime = now; } return element; } async function fillPrompt(promptText) { let promptEditableDiv = findPromptEditableDiv(); if (!promptEditableDiv) { let basePromptButton = Array.from(document.querySelectorAll('button')) .find(btn => btn.textContent.trim() === 'Base Prompt'); if (!basePromptButton) { try { const xpath = '//*[@id="__next"]/div[2]/div[3]/div[3]/div/div[1]/div[3]/div[2]/div/div[1]/div[1]/div[1]/div[2]/button'; basePromptButton = getElementByXPath(xpath); } catch (e) { console.error('查找Base Prompt按钮失败:', e); } } if (basePromptButton) { basePromptButton.click(); await sleep(500); clearDomCache(); promptEditableDiv = findPromptEditableDiv(); } } if (!promptEditableDiv) { updateStatus('错误: 找不到主提示词输入框', 'error'); return false; } const paragraphs = promptEditableDiv.querySelectorAll('p'); if (paragraphs.length > 1) { for (let i = 1; i < paragraphs.length; i++) { paragraphs[i].remove(); } } const promptElement = promptEditableDiv.querySelector('p') || document.createElement('p'); if (!promptElement.parentNode) { promptEditableDiv.appendChild(promptElement); } const template = document.getElementById('preset-prompt-input').value; promptElement.textContent = template.includes('[替换词]') ? template.replace('[替换词]', promptText) : promptText; promptEditableDiv.dispatchEvent(new Event('input', { bubbles: true })); return true; } function findGenerateButton() { const now = Date.now(); if (domCache.generateButton && (now - domCache.lastCacheTime < domCache.cacheTimeout)) { if (document.contains(domCache.generateButton)) { return domCache.generateButton; } } const button = Array.from(document.querySelectorAll('button')) .find(btn => btn.textContent.trim().toLowerCase().startsWith('generate')); if (button) { domCache.generateButton = button; domCache.lastCacheTime = now; } return button; } async function waitForGeneration() { return new Promise((resolve, reject) => { const timeout = 20000; 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('生成超时 (20秒)')); } }, interval); }); } async function checkForNAIError() { await sleep(500); const errorNotification = document.querySelector('.Toastify__toast--error'); return !!errorNotification; } // ========== 核心处理逻辑(添加进度显示)========== async function processSinglePrompt(promptText, statusMsg, imagesPerPrompt, delay, maxRetries, currentIndex, totalCount) { for (let i = 0; i < imagesPerPrompt; i++) { if (!isAutoRunning) return false; let generationSuccessful = false; for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { if (!isAutoRunning) return false; try { const attemptMsg = attempt > 1 ? ` (重试 ${attempt - 1}/${maxRetries})` : ''; updateStatus(`${statusMsg}, 生成第 ${i + 1}/${imagesPerPrompt} 张...${attemptMsg}`); // 更新标题进度 if (titleElement) { titleElement.textContent = `提示词批量助手 [${currentIndex}/${totalCount} 运行中...]`; } if (!await handleCharacterPrompt()) { throw new Error('处理角色模板失败'); } if (!(await fillPrompt(promptText))) { throw new Error('填充主提示词失败'); } await sleep(200); const generateButton = findGenerateButton(); if (!generateButton || generateButton.disabled) { throw new Error('生成按钮不可用'); } generateButton.click(); await waitForGeneration(); if (await checkForNAIError()) { throw new Error('检测到NAI错误提示'); } generationSuccessful = true; break; } catch (error) { console.error('生成失败:', error); updateStatus(`错误: ${error.message}, 正在准备重试...`, 'error'); if (attempt > maxRetries) { updateStatus(`错误: ${error.message},重试次数已用尽,任务中止`, 'error'); stopAutomation(); return false; } clearDomCache(); await sleep(3000); } } if (!generationSuccessful) { updateStatus(`处理提示词失败,任务已停止。`, 'error'); stopAutomation(); return false; } if (!isAutoRunning) return false; updateStatus(`${statusMsg}, 第 ${i + 1}/${imagesPerPrompt} 张图生成完毕,等待 ${delay / 1000} 秒...`); await sleep(delay); } promptsProcessedThisRun++; document.getElementById('run-counter-display').textContent = `本次运行: ${promptsProcessedThisRun} 条`; return true; } // ========== 筛选功能 ========== function applyFilter() { const keyword = document.getElementById('filter-keyword-input').value.trim(); const labelEl = document.getElementById('filter-label'); labelEl.className = ''; if (promptsArray.length === 0) { filteredPromptsArray = []; labelEl.textContent = '3. 从文件中筛选 (请先加载文件)'; labelEl.classList.add('error'); return; } if (!keyword) { filteredPromptsArray = [...promptsArray]; labelEl.textContent = '3. 从文件中筛选 (未启用)'; GM_setValue(STORAGE_KEYS.FILTER_KEYWORD, ''); return; } filteredPromptsArray = promptsArray.filter(p => p.trim().toLowerCase().includes(keyword.toLowerCase()) ); GM_setValue(STORAGE_KEYS.FILTER_KEYWORD, keyword); labelEl.textContent = `3. 从文件中筛选: ${keyword} (${filteredPromptsArray.length}条)`; if (filteredPromptsArray.length > 0) { labelEl.classList.add('done'); } else { labelEl.classList.add('error'); } } // ========== 设置加载 ========== function loadSettings() { const isRandom = GM_getValue(STORAGE_KEYS.RANDOM_MODE, true); document.getElementById('random-mode-checkbox').checked = isRandom; toggleRandomModeUI(isRandom); const presetPromptTextarea = document.getElementById('preset-prompt-input'); presetPromptTextarea.value = GM_getValue(STORAGE_KEYS.PROMPT_TEMPLATE, ''); presetPromptTextarea.scrollTop = 0; const charPromptTextarea = document.getElementById('character-prompt-input'); charPromptTextarea.value = GM_getValue(STORAGE_KEYS.CHARACTER_PROMPT_TEMPLATE, ''); charPromptTextarea.scrollTop = 0; setTimeout(() => { adjustTextareaHeight(presetPromptTextarea); adjustTextareaHeight(charPromptTextarea); }, 100); document.getElementById('filter-keyword-input').value = GM_getValue(STORAGE_KEYS.FILTER_KEYWORD, ''); const savedFileName = GM_getValue(STORAGE_KEYS.LAST_FILE_NAME); const savedFileContent = GM_getValue(STORAGE_KEYS.LAST_FILE_CONTENT); if (savedFileName && savedFileContent) { promptsArray = savedFileContent.split(/\r?\n/).filter(line => line.trim() !== ''); if (promptsArray.length > 0) { document.getElementById('start-index-input').max = promptsArray.length; updateFileInfoDisplay(savedFileName, promptsArray.length); updateStatus(`已自动加载记忆的文件`, 'info'); applyFilter(); } } } // ========== 事件监听 ========== function addEventListeners() { document.getElementById('prompt-file-input').addEventListener('change', handleFileSelect); document.getElementById('main-control-btn').addEventListener('click', mainButtonHandler); document.getElementById('apply-filter-btn').addEventListener('click', applyFilter); document.getElementById('filter-keyword-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') applyFilter(); }); // 标题点击切换折叠 if (titleElement) { titleElement.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(); lastInteractionTime = Date.now(); // 记录交互时间 }); } // 面板内部点击时记录交互时间 if (panelElement) { panelElement.addEventListener('click', () => { lastInteractionTime = Date.now(); }); } const mainInput = document.getElementById('start-index-input'); mainInput.addEventListener('change', () => { const isRandom = document.getElementById('random-mode-checkbox').checked; GM_setValue(isRandom ? STORAGE_KEYS.RANDOM_COUNT : STORAGE_KEYS.START_INDEX, mainInput.value); }); const presetPromptTextarea = document.getElementById('preset-prompt-input'); presetPromptTextarea.addEventListener('input', (e) => { GM_setValue(STORAGE_KEYS.PROMPT_TEMPLATE, e.target.value); adjustTextareaHeight(e.target); }); const charPromptTextarea = document.getElementById('character-prompt-input'); charPromptTextarea.addEventListener('input', (e) => { GM_setValue(STORAGE_KEYS.CHARACTER_PROMPT_TEMPLATE, e.target.value); adjustTextareaHeight(e.target); }); const randomCheckbox = document.getElementById('random-mode-checkbox'); randomCheckbox.addEventListener('change', (e) => { toggleRandomModeUI(e.target.checked); GM_setValue(STORAGE_KEYS.RANDOM_MODE, e.target.checked); }); } function toggleRandomModeUI(isRandom) { const input = document.getElementById('start-index-input'); const label = document.getElementById('start-index-label'); if (isRandom) { label.textContent = "随机次数:"; input.value = GM_getValue(STORAGE_KEYS.RANDOM_COUNT, '50'); input.max = 999; } else { label.textContent = "文件行数:"; input.value = GM_getValue(STORAGE_KEYS.START_INDEX, '1'); input.max = promptsArray.length > 0 ? promptsArray.length : 9999; } } // ========== 文件处理 ========== function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const content = e.target.result; promptsArray = content.split(/\r?\n/).filter(line => line.trim() !== ''); if (promptsArray.length > 0) { if (!document.getElementById('random-mode-checkbox').checked) { document.getElementById('start-index-input').max = promptsArray.length; } updateFileInfoDisplay(file.name, promptsArray.length); updateStatus(`新文件加载成功,准备就绪`, 'info'); GM_setValue(STORAGE_KEYS.LAST_FILE_NAME, file.name); GM_setValue(STORAGE_KEYS.LAST_FILE_CONTENT, content); applyFilter(); } else { updateFileInfoDisplay('文件为空或格式错误', 0, true); updateStatus('请选择一个有效的 .txt 文件', 'error'); } event.target.value = null; }; reader.onerror = () => { updateStatus('文件读取失败', 'error'); }; reader.readAsText(file); } function updateFileInfoDisplay(fileName, count, isError = false) { const display = document.getElementById('file-info-display'); display.textContent = isError ? fileName : `文件: ${fileName} (${count}条)`; display.style.color = isError ? '#e06c75' : '#98c379'; } // ========== 自动化控制 ========== function mainButtonHandler() { isAutoRunning ? stopAutomation() : startAutomation(); } function startAutomation() { const fileContent = GM_getValue(STORAGE_KEYS.LAST_FILE_CONTENT, ''); promptsArray = fileContent.split(/\r?\n/).filter(line => line.trim() !== ''); if (promptsArray.length === 0) { alert('请先选择或自动加载一个有效的提示词文件!'); return; } applyFilter(); if (filteredPromptsArray.length === 0) { alert('筛选结果为空,无法开始任务!请更改或清空筛选词。'); return; } isAutoRunning = true; promptsProcessedThisRun = 0; clearDomCache(); document.getElementById('run-counter-display').textContent = '本次运行: 0 条'; document.getElementById('main-control-btn').textContent = '⏹️ 停止执行'; document.getElementById('main-control-btn').className = 'running'; const imagesPerPrompt = parseInt(document.getElementById('images-per-prompt-input').value, 10); const delay = parseFloat(document.getElementById('delay-input').value) * 1000; const isRandom = document.getElementById('random-mode-checkbox').checked; const maxRetries = parseInt(document.getElementById('max-retries-input').value, 10); if (isRandom) { const randomCount = parseInt(document.getElementById('start-index-input').value, 10); runRandomAutomation(randomCount, imagesPerPrompt, delay, maxRetries); } else { const startIndex = Math.max(0, parseInt(document.getElementById('start-index-input').value, 10) - 1); const sequenceToProcess = filteredPromptsArray.filter(prompt => { const originalIndex = promptsArray.indexOf(prompt); return originalIndex >= startIndex; }); if (sequenceToProcess.length === 0) { alert(`从第 ${startIndex + 1} 行开始,未找到符合筛选条件的提示词。`); stopAutomation(); return; } runSequentialAutomation(sequenceToProcess, imagesPerPrompt, delay, maxRetries); } } function stopAutomation() { isAutoRunning = false; document.getElementById('main-control-btn').textContent = '▶️ 开始执行'; document.getElementById('main-control-btn').className = ''; if (titleElement) { titleElement.textContent = '提示词批量助手'; } updateStatus('任务已手动停止', 'info'); } async function runSequentialAutomation(sequenceToProcess, imagesPerPrompt, delay, maxRetries) { for (let i = 0; i < sequenceToProcess.length; i++) { if (!isAutoRunning) break; const currentPrompt = sequenceToProcess[i]; const originalIndex = promptsArray.indexOf(currentPrompt); const statusMsg = `顺序(筛选后): 第 ${i + 1}/${sequenceToProcess.length} 条 (原文件第 ${originalIndex + 1} 行)`; if (!await processSinglePrompt(currentPrompt, statusMsg, imagesPerPrompt, delay, maxRetries, i + 1, sequenceToProcess.length)) { return; } if (isAutoRunning) { const nextIndex = originalIndex + 1; document.getElementById('start-index-input').value = nextIndex + 1; GM_setValue(STORAGE_KEYS.START_INDEX, (nextIndex + 1).toString()); } } if (isAutoRunning) { updateStatus(`顺序(筛选后)任务处理完毕!`, 'done'); stopAutomation(); } } async function runRandomAutomation(randomCount, imagesPerPrompt, delay, maxRetries) { for (let i = 0; i < randomCount; i++) { if (!isAutoRunning) break; const randomFilteredIndex = Math.floor(Math.random() * filteredPromptsArray.length); const selectedPrompt = filteredPromptsArray[randomFilteredIndex]; const originalIndex = promptsArray.indexOf(selectedPrompt); const statusMsg = `随机 (筛选后): 第 ${i + 1}/${randomCount} 次 (抽中原文件第 ${originalIndex + 1} 行)`; if (!await processSinglePrompt(selectedPrompt, statusMsg, imagesPerPrompt, delay, maxRetries, i + 1, randomCount)) { return; } } if (isAutoRunning) { updateStatus(`随机任务 ${randomCount} 次已全部完成!`, 'done'); stopAutomation(); } } function updateStatus(message, type = 'info') { const el = document.getElementById('status-display'); if (el) { el.textContent = message; el.className = type; } } // ========== 样式 ========== GM_addStyle(` #nai-prompt-helper-panel { position: fixed; top: 70px; right: 25px; z-index: 9999; width: 320px; background: #1c1f26; color: #c8ccd4; border-radius: 12px; border: 1px solid #3a414f; box-shadow: 0 8px 25px rgba(0,0,0,0.3); font-family: "Segoe UI", sans-serif; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; } #nai-prompt-helper-panel.collapsed { transform: translateY(calc(100vh - 180px)); opacity: 0.85; } #nai-prompt-helper-panel.collapsed:hover { opacity: 1; } #nai-prompt-helper-panel h3 { padding: 12px 16px; border-bottom: 1px solid #3a414f; cursor: pointer; user-select: none; display: flex; justify-content: space-between; align-items: center; color: #82aaff; font-size: 16px; font-weight: 600; margin: 0; transition: background-color 0.2s; } #nai-prompt-helper-panel h3:hover { background-color: rgba(130, 170, 255, 0.1); } #nai-prompt-helper-panel.collapsed h3 { border-bottom: none; } #nai-prompt-helper-panel h3::after { content: '−'; font-size: 20px; transition: transform 0.3s; } #nai-prompt-helper-panel.collapsed h3::after { content: '+'; transform: rotate(90deg); } #nai-prompt-helper-panel .content-wrapper { padding: 16px; display: flex; flex-direction: column; gap: 14px; max-height: calc(80vh - 15px); overflow-y: auto; overflow-x: hidden; transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease; } #nai-prompt-helper-panel .content-wrapper::-webkit-scrollbar { width: 8px; } #nai-prompt-helper-panel .content-wrapper::-webkit-scrollbar-track { background: #282c34; border-radius: 4px; } #nai-prompt-helper-panel .content-wrapper::-webkit-scrollbar-thumb { background: #4f586a; border-radius: 4px; } #nai-prompt-helper-panel .content-wrapper::-webkit-scrollbar-thumb:hover { background: #5a657a; } #nai-prompt-helper-panel.collapsed .content-wrapper { max-height: 0; padding: 0 16px; opacity: 0; } #nai-prompt-helper-panel hr { border: none; border-top: 1px solid #3a414f; margin: 4px 0; } #nai-prompt-helper-panel .control-group { display: flex; flex-direction: column; gap: 8px; } #nai-prompt-helper-panel .file-controls { display: flex; align-items: center; gap: 12px; } #nai-prompt-helper-panel label { font-size: 14px; color: #a6accd; transition: color 0.2s; } #nai-prompt-helper-panel .custom-file-button { background-color: #4f586a; border-radius: 6px; padding: 0; cursor: pointer; transition: all 0.2s; width: 38px; height: 38px; display: flex; justify-content: center; align-items: center; flex-shrink: 0; } #nai-prompt-helper-panel .custom-file-button:hover { background-color: #5a657a; transform: scale(1.05); } #nai-prompt-helper-panel #file-info-display { font-size: 13px; text-align: left; padding: 6px 10px; background-color: #282c34; border-radius: 6px; border: 1px dashed #4f586a; word-break: break-all; margin-top: 0; flex-grow: 1; transition: all 0.3s; } #nai-prompt-helper-panel .heading-with-control { display: flex; justify-content: space-between; align-items: center; } #nai-prompt-helper-panel .toggle-switch-wrapper { display: flex; align-items: center; gap: 8px; } #nai-prompt-helper-panel .toggle-switch-label { font-size: 14px; color: #a6accd; } #nai-prompt-helper-panel .toggle-switch { position: relative; display: inline-block; width: 40px; height: 22px; } #nai-prompt-helper-panel .toggle-switch input { opacity: 0; width: 0; height: 0; } #nai-prompt-helper-panel .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #4f586a; border-radius: 22px; transition: .3s; } #nai-prompt-helper-panel .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; border-radius: 50%; transition: .3s; } #nai-prompt-helper-panel input:checked + .slider { background-color: #82aaff; } #nai-prompt-helper-panel input:checked + .slider:before { transform: translateX(18px); } #nai-prompt-helper-panel input, #nai-prompt-helper-panel textarea, #nai-prompt-helper-panel button { width: 100%; padding: 10px; box-sizing: border-box; font-size: 14px; border-radius: 6px; border: 1px solid #4f586a; background-color: #282c34; color: #c8ccd4; transition: all 0.2s; } #nai-prompt-helper-panel input:focus, #nai-prompt-helper-panel textarea:focus { outline: none; border-color: #82aaff; box-shadow: 0 0 0 2px rgba(130, 170, 255, 0.2); } #nai-prompt-helper-panel textarea { resize: vertical; } #nai-prompt-helper-panel #preset-prompt-input { min-height: 100px; max-height: 140px; } #nai-prompt-helper-panel #character-prompt-input { min-height: 45px; max-height: 60px; } #nai-prompt-helper-panel .settings { flex-direction: row; justify-content: space-between; gap: 8px; flex-wrap: wrap; } #nai-prompt-helper-panel .settings > div { flex-basis: calc(25% - 6px); text-align: center; } #nai-prompt-helper-panel .settings label { font-size: 13px; margin-bottom: 4px; display: block; } #nai-prompt-helper-panel .settings input { width: 100%; padding: 8px; text-align: center; } #nai-prompt-helper-panel #main-control-btn { font-weight: bold; color: #f1f1f1; background-color: #82aaff; border-color: #82aaff; cursor: pointer; transition: all 0.2s; } #nai-prompt-helper-panel #main-control-btn:hover { background-color: #90b8ff; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(130, 170, 255, 0.3); } #nai-prompt-helper-panel #main-control-btn.running { background-color: #e06c75; border-color: #e06c75; } #nai-prompt-helper-panel #main-control-btn.running:hover { background-color: #f07f88; } #nai-prompt-helper-panel #status-display { padding: 10px; border-radius: 6px; font-size: 13px; text-align: center; word-wrap: break-word; line-height: 1.4; background-color: #282c34; transition: all 0.3s; } #nai-prompt-helper-panel #run-counter-display { font-size: 13px; text-align: center; color: #98c379; } #nai-prompt-helper-panel #status-display.error { background-color: rgba(224, 108, 117, 0.2); color: #e06c75; font-weight: bold; border: 1px solid #e06c75; } #nai-prompt-helper-panel #filter-label.error { color: #e06c75 !important; } #nai-prompt-helper-panel #status-display.done { background-color: rgba(152, 195, 121, 0.2); color: #98c379; font-weight: bold; border: 1px solid #98c379; } #nai-prompt-helper-panel #filter-label.done { color: #98c379 !important; } #nai-prompt-helper-panel .input-with-button { display: flex; gap: 8px; } #nai-prompt-helper-panel #filter-keyword-input { flex-grow: 1; } #nai-prompt-helper-panel #apply-filter-btn { flex-grow: 0; flex-shrink: 0; width: auto; padding: 0 15px; background-color: #4f586a; border: none; cursor: pointer; transition: all 0.2s; } #nai-prompt-helper-panel #apply-filter-btn:hover { background-color: #5a657a; transform: scale(1.05); } `); // ========== 初始化 ========== function waitForNAI() { let attempts = 0; const maxAttempts = 30; const checkInterval = setInterval(() => { attempts++; const generateBtn = findGenerateButton(); if (generateBtn) { clearInterval(checkInterval); createUIPanel(); removeUnwantedElement(); } else if (attempts >= maxAttempts) { clearInterval(checkInterval); console.error("NAI 提示词批量助手:等待界面超时"); alert("NAI 提示词批量助手:等待界面超时,脚本可能需要更新。"); } }, 2000); } waitForNAI(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址