// ==UserScript== // @name 豆包自动批量发送 (v0.1.1) // @namespace https://scriptcat.org/zh-CN // @version 0.1.1 // @description 豆包自动批量发送,支持单条和多条发送与自动下载 // @author AI 辅助完成 // @match https://www.doubao.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_setClipboard // @grant GM_download // @grant GM_xmlhttpRequest // @connect byteimg.com // @connect * // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY_PROMPTS = "db_img_prompts"; const STORAGE_KEY_MIN = "db_interval_min"; const STORAGE_KEY_MAX = "db_interval_max"; const STORAGE_KEY_BATCH = "db_batch_size"; const STORAGE_KEY_AUTODL = "db_auto_download"; const STORAGE_KEY_DL_OLD = "db_dl_historical"; const STORAGE_KEY_SUBFOLDER = "db_dl_subfolder"; const style = document.createElement('style'); style.innerHTML = ` #db-auto-sidebar { position: fixed; top: 0; right: 0; width: 380px; height: 100vh; background: #ffffff; box-shadow: -5px 0 25px rgba(0,0,0,0.15); z-index: 99999; display: flex; flex-direction: column; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); transform: translateX(0); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } #db-auto-sidebar.hidden { transform: translateX(100%); } #db-sidebar-trigger { position: fixed; top: 50%; right: 0; transform: translateY(-50%) translateX(100%); background: #3b82f6; color: white; padding: 20px 8px; border-radius: 8px 0 0 8px; cursor: pointer; z-index: 99998; font-size: 14px; font-weight: bold; line-height: 1.5; box-shadow: -2px 0 10px rgba(0,0,0,0.1); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: flex; align-items: center; justify-content: center; text-align: center; } #db-sidebar-trigger.visible { transform: translateY(-50%) translateX(0); } #db-sidebar-header { padding: 18px 20px; border-bottom: 1px solid #edf2f7; background: #f8fafc; display: flex; justify-content: space-between; align-items: center; } #db-sidebar-body { padding: 20px; display: flex; flex-direction: column; flex: 1; overflow: hidden; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } .setting-row { font-size: 13px; margin-bottom: 8px; color: #475569; display: flex; justify-content: space-between; align-items: center; background: #f8fafc; padding: 10px 12px; border: 1px solid #e2e8f0; border-radius: 6px; } .setting-row.highlight { background: #eff6ff; border-color: #bfdbfe; } `; document.head.appendChild(style); const container = document.createElement('div'); container.innerHTML = `



🤖 智控画图终端

收起 ➔
待发任务队列 待处理 0
发送模式:
自动下载新图 (雷达模式)
分类标签:
自动下载历史旧图
画完冷却: -
📡 实时监控中心
`; document.body.appendChild(container); const sidebar = document.getElementById('db-auto-sidebar'), triggerBtn = document.getElementById('db-sidebar-trigger'), hideBtn = document.getElementById('hide-sidebar-btn'), startBtn = document.getElementById('start-send'), appendBtn = document.getElementById('append-btn'), resetBtn = document.getElementById('reset-progress'), promptInput = document.getElementById('prompt-list'), promptCount = document.getElementById('prompt-count'), logPanel = document.getElementById('log-panel'), minInput = document.getElementById('min-interval'), maxInput = document.getElementById('max-interval'), batchSelect = document.getElementById('send-batch-size'), autoDlCheckbox = document.getElementById('auto-download-cb'), historicalDlCheckbox = document.getElementById('dl-historical-cb'), subfolderInput = document.getElementById('subfolder-name'); let isRunning = false; let isCurrentlyGenerating = false; let globalTaskQueue = []; // 核心新增:独立运行任务队列 const processedImageMemory = new Set(); hideBtn.onclick = () => { sidebar.classList.add('hidden'); triggerBtn.classList.add('visible'); }; triggerBtn.onclick = () => { sidebar.classList.remove('hidden'); triggerBtn.classList.remove('visible'); }; // --- 热更新队列计数与持久化引擎 --- function updatePromptCount() { const validPrompts = promptInput.value.split('\n').filter(p => p.trim() !== ''); if (isRunning) { // 运行期:总数 = 后台排队数 + 前台新敲的词数 promptCount.innerText = globalTaskQueue.length + validPrompts.length; // 合并存储,防刷新丢失 const fullQueue = globalTaskQueue.concat(validPrompts); GM_setValue(STORAGE_KEY_PROMPTS, fullQueue.join('\n')); } else { promptCount.innerText = validPrompts.length; GM_setValue(STORAGE_KEY_PROMPTS, promptInput.value); } } promptInput.addEventListener('input', updatePromptCount); // 追加按钮功能 appendBtn.onclick = () => { let newPrompts = promptInput.value.split('\n').filter(p => p.trim() !== ''); if (newPrompts.length > 0) { globalTaskQueue.push(...newPrompts); // 追加至后台队列尾部 promptInput.value = ''; // 清空输入框以便下次输入 updatePromptCount(); addLog(`➕ 成功追加 ${newPrompts.length} 条提示词到队列!`, 'success'); } }; autoDlCheckbox.addEventListener('change', () => GM_setValue(STORAGE_KEY_AUTODL, autoDlCheckbox.checked)); historicalDlCheckbox.addEventListener('change', () => GM_setValue(STORAGE_KEY_DL_OLD, historicalDlCheckbox.checked)); batchSelect.addEventListener('change', () => GM_setValue(STORAGE_KEY_BATCH, batchSelect.value)); subfolderInput.addEventListener('input', () => GM_setValue(STORAGE_KEY_SUBFOLDER, subfolderInput.value)); function addLog(msg, type = 'info') { const now = new Date(); const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`; const logEntry = document.createElement('div'); logEntry.style.marginBottom = '4px'; logEntry.style.color = type === 'error' ? '#f87171' : (type === 'success' ? '#4ade80' : (type === 'highlight' ? '#fcd34d' : (type === 'dl' ? '#38bdf8' : '#94a3b8'))); logEntry.innerText = `[${timeStr}] ${msg}`; logPanel.appendChild(logEntry); logPanel.scrollTop = logPanel.scrollHeight; } setTimeout(() => { const oldImages = document.querySelectorAll('img[src*="rc_gen_image"]'); oldImages.forEach(img => { let key = img.src.split('?')[0]; processedImageMemory.add(key); }); }, 2500); // ========================================== // 核心黑科技:Blob 内存提取 + 子文件夹强绑 // ========================================== function downloadImageAsBlob(imgUrl, fileName, btnFallback) { GM_xmlhttpRequest({ method: 'GET', url: imgUrl, responseType: 'blob', onload: function(response) { if (response.status === 200) { const blobUrl = URL.createObjectURL(response.response); GM_download({ url: blobUrl, name: fileName, saveAs: false, onload: () => { URL.revokeObjectURL(blobUrl); }, onerror: (err) => { URL.revokeObjectURL(blobUrl); btnFallback.click(); } }); } else { btnFallback.click(); } }, onerror: function() { btnFallback.click(); } }); } setInterval(async () => { const breakBtn = document.querySelector('[data-testid="chat_input_local_break_button"]'); const autoDlChecked = autoDlCheckbox.checked; const dlHistorical = historicalDlCheckbox.checked; const rawTargetFolder = subfolderInput.value.trim(); if (breakBtn) isCurrentlyGenerating = true; const btns = document.querySelectorAll('[data-testid="edit_image_hover_tag_download_btn"]'); if (btns.length > 0) { if (isCurrentlyGenerating || isRunning) { if (autoDlChecked && !breakBtn) { let newFoundCount = 0; for (let btn of btns) { let pNode = btn.parentElement; let imgNode = null; for(let k = 0; k < 6; k++) { if(!pNode) break; imgNode = pNode.querySelector('img[src*="rc_gen_image"]'); if(imgNode) break; pNode = pNode.parentElement; } if (imgNode && imgNode.src) { let imgKey = imgNode.src.split('?')[0]; if (processedImageMemory.has(imgKey)) continue; processedImageMemory.add(imgKey); newFoundCount++; if (rawTargetFolder === "") { btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); btn.click(); } else { const safeName = rawTargetFolder.replace(/[\/\\:*?"<>|]/g, ''); const ts = new Date().getTime(); const rn = Math.floor(Math.random() * 10000); const customFileName = `${safeName}/豆包原画_${ts}_${rn}.png`; downloadImageAsBlob(imgNode.src, customFileName, btn); } await new Promise(r => setTimeout(r, 600)); } } if (newFoundCount > 0) addLog(`✅ 成功保存 ${newFoundCount} 张图片!`, 'success'); isCurrentlyGenerating = false; } } else { if (dlHistorical && autoDlChecked) { let historicalFound = 0; for (let btn of btns) { let pNode = btn.parentElement; let imgNode = null; for(let k = 0; k < 6; k++) { if(!pNode) break; imgNode = pNode.querySelector('img[src*="rc_gen_image"]'); if(imgNode) break; pNode = pNode.parentElement; } if (imgNode && imgNode.src) { let imgKey = imgNode.src.split('?')[0]; if (!processedImageMemory.has(imgKey)) { processedImageMemory.add(imgKey); historicalFound++; btn.click(); await new Promise(r => setTimeout(r, 500)); } } } } else { btns.forEach(btn => { let pNode = btn.parentElement; let imgNode = null; for(let k = 0; k < 6; k++) { if(!pNode) break; imgNode = pNode.querySelector('img[src*="rc_gen_image"]'); if(imgNode) break; pNode = pNode.parentElement; } if (imgNode && imgNode.src) processedImageMemory.add(imgNode.src.split('?')[0]); }); } } } }, 1500); // --- 核心发送引擎 --- async function trySwitchToImageMode() { if (document.body.innerText.includes("描述你想要的图片")) return true; const elements = document.querySelectorAll('div.min-w-0.truncate'); let targetMenu = Array.from(elements).find(el => el.innerText.trim() === "图像生成"); if (targetMenu) { let p = targetMenu; for (let i = 0; i < 4; i++) { if (p) { p.click(); p = p.parentElement; } } await new Promise(r => setTimeout(r, 2000)); } return true; } async function waitUntilFinished() { addLog("正在监控画师进度...", 'dim'); await new Promise(r => setTimeout(r, 3000)); let waitTime = 3; while (waitTime < 180) { if (!isRunning) return false; if (!document.querySelector('[data-testid="chat_input_local_break_button"]')) return true; await new Promise(r => setTimeout(r, 2000)); waitTime += 2; } return true; } async function simulateInputAndSend(text) { const textAreaEditor = document.querySelector('textarea[data-testid="chat_input_input"]'); const slateEditors = document.querySelectorAll('[contenteditable="true"]'); let editor = Array.from(slateEditors).find(el => el.innerText.includes("描述你想要的图片") || el.getAttribute('data-slate-editor') === 'true') || slateEditors[0] || textAreaEditor; if (!editor) return false; editor.focus(); const dt = new DataTransfer(); dt.setData('text/plain', text); editor.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true })); await new Promise(r => setTimeout(r, 200)); if (!(editor.innerText || editor.value || "").includes(text.substring(0,5))) document.execCommand('insertText', false, text); editor.dispatchEvent(new Event('input', { bubbles: true })); await new Promise(r => setTimeout(r, 800)); const sendBtn = document.getElementById('flow-end-msg-send') || document.querySelector('button[data-testid="chat_input_send_button"]'); if (sendBtn && sendBtn.getAttribute('data-disabled') !== 'true') sendBtn.click(); else editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })); for (let i = 0; i < 10; i++) { await new Promise(r => setTimeout(r, 500)); let val = editor.value || editor.innerText || ""; if (!val || val.trim() === "" || val.includes("描述你想要的图片")) return true; } return false; } // 主流程控制 startBtn.onclick = async () => { if (isRunning) { // 停止逻辑 isRunning = false; startBtn.innerText = '▶ 开始运行'; startBtn.style.background = '#3b82f6'; resetBtn.style.display = 'block'; appendBtn.style.display = 'none'; promptInput.placeholder = "每行一条提示词..."; // 将未发送的队列吐回输入框 const currentVal = promptInput.value.trim(); const remaining = globalTaskQueue.join('\n'); promptInput.value = currentVal ? remaining + '\n' + currentVal : remaining; globalTaskQueue = []; updatePromptCount(); addLog("⏸ 系统已挂起暂停", 'highlight'); return; } // 启动逻辑 let prompts = promptInput.value.split('\n').filter(p => p.trim() !== ''); if (prompts.length === 0) return alert("请先输入提示词"); globalTaskQueue = prompts; // 移交至后台队列 isRunning = true; startBtn.innerText = '⏸ 停止运行'; startBtn.style.background = '#ef4444'; // 界面变身:隐藏重置,显示追加 resetBtn.style.display = 'none'; appendBtn.style.display = 'block'; promptInput.placeholder = "运行中...在此输入新提示词并点击【➕ 追加】"; const batchSize = parseInt(batchSelect.value); promptInput.value = ""; // 清空框用于新输入 updatePromptCount(); addLog(`=== 全自动引擎启动,连发: ${batchSize} ===`, 'success'); await trySwitchToImageMode(); while (globalTaskQueue.length > 0 && isRunning) { let currentBatch = globalTaskQueue.splice(0, batchSize); let currentPromptText = currentBatch.join('\n'); updatePromptCount(); // 切割后立刻更新计数 addLog(`📊 进度: 队列剩余 ${globalTaskQueue.length} 条`, 'highlight'); if (await simulateInputAndSend(currentPromptText)) { await waitUntilFinished(); if (globalTaskQueue.length > 0 && isRunning) { const wait = Math.floor(Math.random() * (parseInt(maxInput.value) - parseInt(minInput.value) + 1) + parseInt(minInput.value)); addLog(`冷却:${wait} 秒...`, 'dim'); for (let s = wait; s > 0; s--) { if (!isRunning) break; await new Promise(r => setTimeout(r, 1000)); } } } else { addLog(`❌ 发送异常`, 'error'); globalTaskQueue = currentBatch.concat(globalTaskQueue); isRunning = false; break; } } // 执行完毕善后 if (isRunning && globalTaskQueue.length === 0) { addLog("🎉 任务处理完毕!", 'success'); isRunning = false; } if (!isRunning) { startBtn.innerText = '▶ 开始运行'; startBtn.style.background = '#3b82f6'; resetBtn.style.display = 'block'; appendBtn.style.display = 'none'; promptInput.placeholder = "每行一条提示词..."; // 确保任务异常中止时退回提示词 if (globalTaskQueue.length > 0) { const currentVal = promptInput.value.trim(); const remaining = globalTaskQueue.join('\n'); promptInput.value = currentVal ? remaining + '\n' + currentVal : remaining; globalTaskQueue = []; } updatePromptCount(); } }; document.getElementById('reset-progress').onclick = () => { if (confirm("确定重置?")) { promptInput.value = ""; globalTaskQueue = []; updatePromptCount(); logPanel.innerHTML = ""; isRunning = false; } }; document.getElementById('copy-logs').onclick = () => { GM_setClipboard(logPanel.innerText); alert("日志已复制"); }; // 初始读取状态 promptInput.value = GM_getValue(STORAGE_KEY_PROMPTS, ""); updatePromptCount(); batchSelect.value = GM_getValue(STORAGE_KEY_BATCH, "1"); autoDlCheckbox.checked = GM_getValue(STORAGE_KEY_AUTODL, true); historicalDlCheckbox.checked = GM_getValue(STORAGE_KEY_DL_OLD, false); subfolderInput.value = GM_getValue(STORAGE_KEY_SUBFOLDER, ""); })();