// ==UserScript== // @name Douyin-Auto-Keep-Fire(抖音自动续火花脚本) // @namespace Douyin-Auto-Keep-Fire // @version 2.0 // @description 每日自动发送续火花消息,允许自定义用户、消息内容、发送时间,支持TextAPI、记录火花天数等。 // @author 東亰藍調(iEastBlues) // @match https://www.douyin.com/chat* // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license MIT // @homepageURL https://i.eastblues.cn/Douyin-Auto-Keep-Fire.html // ==/UserScript== (function () { 'use strict'; const STORAGE_KEYS = { CONFIG: 'douyin_spark_auto_config', SEND_RECORD: 'douyin_spark_send_record', }; const DOM_SELECTORS = { SEARCH_INPUT: 'input.semi-input[placeholder="搜索"][type="text"]', CHAT_BTN: 'div[class*="SearchPanelitemchat_btn"]', SPARK_STATUS: 'div.commonStreaknormalText', CHAT_INPUT: 'div[data-slate-editor="true"][contenteditable="true"]', PAGE_CONTAINER: 'body' }; const GLOBAL_STATE = { config: { targetUsers: [], sendContent: '', sendTime: '', }, todaySendRecord: {}, taskTimer: null, dayResetTimer: null, isTaskRunning: false, currentDate: new Date().toLocaleDateString(), menuCommandId: null, }; function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return format .replace('YYYY', year) .replace('MM', month) .replace('DD', day) .replace('HH', hours) .replace('mm', minutes) .replace('ss', seconds); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function validateTimeFormat(timeStr) { const timeReg = /^([01][0-9]|2[0-3]):([0-5][0-9])$/; return timeReg.test(timeStr); } function isReachSendTime(targetTime) { if (!validateTimeFormat(targetTime)) return false; const [targetHour, targetMinute] = targetTime.split(':').map(Number); const now = new Date(); const currentHour = now.getHours(); const currentMinute = now.getMinutes(); return currentHour > targetHour || (currentHour === targetHour && currentMinute >= targetMinute); } function simulateMouseClick(element) { if (!element || !(element instanceof HTMLElement)) { return false; } const rect = element.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const event = new MouseEvent('click', { bubbles: true, clientX: x, clientY: y }); element.dispatchEvent(event); return true; } async function simulateHumanInput(inputElement, content, interval = 50) { if (!inputElement || !(inputElement instanceof HTMLElement)) { return false; } if (typeof content !== 'string') { return false; } inputElement.focus(); simulateMouseClick(inputElement); await sleep(30); if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') { inputElement.select(); document.execCommand('delete'); } else if (inputElement.isContentEditable) { inputElement.innerHTML = ''; } inputElement.dispatchEvent(new Event('input', { bubbles: true })); inputElement.dispatchEvent(new Event('change', { bubbles: true })); await sleep(50); const charArray = content.split(''); for (let i = 0; i < charArray.length; i++) { const char = charArray[i]; if (inputElement.tagName === 'INPUT' || inputElement.tagName === 'TEXTAREA') { document.execCommand('insertText', false, char); inputElement.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data: char, inputType: 'insertText' })); } else if (inputElement.isContentEditable) { const textNode = document.createTextNode(char); const selection = window.getSelection(); const range = selection.getRangeAt(0); range.insertNode(textNode); range.setStartAfter(textNode); range.setEndAfter(textNode); selection.removeAllRanges(); selection.addRange(range); inputElement.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data: char, inputType: 'insertText' })); } inputElement.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: char })); inputElement.dispatchEvent(new KeyboardEvent('keypress', { bubbles: true, key: char })); inputElement.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: char })); await sleep(interval); } inputElement.dispatchEvent(new Event('change', { bubbles: true })); inputElement.dispatchEvent(new Event('blur', { bubbles: true })); return true; } function waitForElement(selector, timeout = 20000, parent = document) { return new Promise((resolve) => { const existElement = parent.querySelector(selector); if (existElement) { return resolve(existElement); } const interval = 100; let elapsedTime = 0; const timer = setInterval(() => { elapsedTime += interval; const element = parent.querySelector(selector); if (element) { clearInterval(timer); resolve(element); } else if (elapsedTime >= timeout) { clearInterval(timer); resolve(null); } }, interval); }); } function saveConfig() { try { const targetUsersInput = document.getElementById('spark-target-users').value.trim(); const sendContentInput = document.getElementById('spark-send-content').value.trim(); const sendTimeInput = document.getElementById('spark-send-time').value.trim(); if (!targetUsersInput) { alert('Invalid input'); return false; } if (!sendContentInput) { alert('Invalid input'); return false; } if (!validateTimeFormat(sendTimeInput)) { alert('Invalid input Example: 00:30'); return false; } const targetUsers = targetUsersInput.split(',').map(item => item.trim()).filter(item => item); const config = { targetUsers, sendContent: sendContentInput, sendTime: sendTimeInput }; GLOBAL_STATE.config = config; localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(config)); alert('Success'); return true; } catch (error) { alert('Failure'); return false; } } function loadConfig() { try { const configStr = localStorage.getItem(STORAGE_KEYS.CONFIG); if (!configStr) { return; } const config = JSON.parse(configStr); GLOBAL_STATE.config = config; document.getElementById('spark-target-users').value = config.targetUsers.join(', '); document.getElementById('spark-send-content').value = config.sendContent; document.getElementById('spark-send-time').value = config.sendTime; } catch (error) { GLOBAL_STATE.config = { targetUsers: [], sendContent: '', sendTime: '', }; } } function exportConfig() { try { const configStr = JSON.stringify(GLOBAL_STATE.config, null, 2); navigator.clipboard.writeText(configStr).then(() => { alert('Success'); }); } catch (error) { alert('Failure'); } } function importConfig() { const input = prompt('请粘贴配置文件:'); if (!input) return; try { const config = JSON.parse(input); if (!Array.isArray(config.targetUsers) || typeof config.sendContent !== 'string' || typeof config.sendTime !== 'string') { } GLOBAL_STATE.config = config; localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(config)); document.getElementById('spark-target-users').value = config.targetUsers.join(', '); document.getElementById('spark-send-content').value = config.sendContent; document.getElementById('spark-send-time').value = config.sendTime; alert('Success'); } catch (error) { alert('Error:请检查JSON格式并重试'); } } function resetAllData() { const isConfirm = confirm('警告:此操作将清空所有数据,是否继续?'); if (!isConfirm) return; try { Object.values(STORAGE_KEYS).forEach(key => localStorage.removeItem(key)); GLOBAL_STATE.config = { targetUsers: [], sendContent: '', sendTime: '' }; GLOBAL_STATE.todaySendRecord = {}; stopTask(); document.getElementById('spark-target-users').value = ''; document.getElementById('spark-send-content').value = ''; document.getElementById('spark-send-time').value = ''; updateSparkStatusPanel(); alert('Success'); } catch (error) { alert('Failure'); } } function resetRecord() { const isConfirm = confirm('此操作将重置今日发送记录,是否继续?'); if (!isConfirm) return; try { localStorage.removeItem(STORAGE_KEYS.SEND_RECORD); GLOBAL_STATE.todaySendRecord = {}; updateSparkStatusPanel(); alert('Success'); } catch (error) { alert('Failure'); } } async function parseApiContent(content) { if (!content || typeof content !== 'string') return content; const apiReg = /\(API:([^)]+)\)/g; const matches = [...content.matchAll(apiReg)]; if (matches.length === 0) { return content; } let finalContent = content; for (const match of matches) { const [fullMatch, apiUrl] = match; try { const apiResult = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: apiUrl, timeout: 8000, onload: (response) => { if (response.status >= 200 && response.status < 300) { resolve(response.responseText); } else { reject(new Error(`API请求失败,状态码:${response.status}`)); } }, onerror: (error) => reject(new Error(`API请求异常:${error.message}`)), ontimeout: () => reject(new Error(`API请求超时,超时时间8000ms`)) }); }); finalContent = finalContent.replace(fullMatch, apiResult.trim()); } catch (error) { finalContent = finalContent.replace(fullMatch, `[API]`); } } return finalContent; } async function sendMessageToUser(userName, sendContent) { let sparkDays = 0; try { const searchInput = await waitForElement(DOM_SELECTORS.SEARCH_INPUT); if (!searchInput) { return { success: false, sparkDays }; } const inputSuccess = await simulateHumanInput(searchInput, userName); if (!inputSuccess) { return { success: false, sparkDays }; } await sleep(1000); let searchResultContainer = searchInput.closest('div') || searchInput.parentElement; if (!searchResultContainer.querySelector(DOM_SELECTORS.CHAT_BTN)) { let current = searchInput.parentElement; while (current && current !== document.body) { if (current.querySelector(DOM_SELECTORS.CHAT_BTN)) { searchResultContainer = current; break; } current = current.parentElement; } } const chatBtn = await waitForElement(DOM_SELECTORS.CHAT_BTN, 5000, searchResultContainer); if (!chatBtn) { return { success: false, sparkDays }; } const clickSuccess = simulateMouseClick(chatBtn); if (!clickSuccess) { return { success: false, sparkDays }; } await sleep(1500); const chatInput = await waitForElement(DOM_SELECTORS.CHAT_INPUT); if (!chatInput) { return { success: false, sparkDays }; } const contentInputSuccess = await simulateHumanInput(chatInput, sendContent); if (!contentInputSuccess) { return { success: false, sparkDays }; } await sleep(500); const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }); chatInput.dispatchEvent(enterEvent); await sleep(1000); const conversationListWrapper = await waitForElement('.conversationConversationListwrapper', 3000); if (conversationListWrapper) { await sleep(300); conversationListWrapper.scrollTop = 0; } const sparkElement = document.querySelector(DOM_SELECTORS.SPARK_STATUS); if (sparkElement) { sparkDays = parseInt(sparkElement.textContent.trim()) || 0; } return { success: true, sparkDays }; } catch (error) { return { success: false, sparkDays }; } } async function batchSendMessage(userList, originalContent) { if (!userList || userList.length === 0) { return; } const finalContent = await parseApiContent(originalContent); const pendingUsers = userList.filter(user => !GLOBAL_STATE.todaySendRecord[user]?.isSuccess); if (pendingUsers.length === 0) { return; } for (let i = 0; i < pendingUsers.length; i++) { const userName = pendingUsers[i]; const { success, sparkDays } = await sendMessageToUser(userName, finalContent); GLOBAL_STATE.todaySendRecord[userName] = { userName, isSuccess: success, sendTime: formatDate(new Date()), sparkDays: sparkDays }; localStorage.setItem(STORAGE_KEYS.SEND_RECORD, JSON.stringify(GLOBAL_STATE.todaySendRecord)); updateSparkStatusPanel(); if (i < pendingUsers.length - 1) { await sleep(3000); } } } function startTask() { const { targetUsers, sendContent, sendTime } = GLOBAL_STATE.config; if (targetUsers.length === 0 || !sendContent || !validateTimeFormat(sendTime)) { alert('请先填写并保存完整的配置信息'); return; } if (GLOBAL_STATE.taskTimer) clearInterval(GLOBAL_STATE.taskTimer); GLOBAL_STATE.isTaskRunning = true; initTodaySendRecord(); const startBtn = document.getElementById('spark-task-start-btn'); startBtn.textContent = '停止'; startBtn.style.background = '#ff3b30'; startBtn.style.color = '#fff'; checkAndSend(); updateSparkStatusPanel(); GLOBAL_STATE.taskTimer = setInterval(() => { const nowDate = new Date().toLocaleDateString(); if (nowDate !== GLOBAL_STATE.currentDate) { crossDayReset(); } checkAndSend(); }, 30 * 1000); } function stopTask() { if (GLOBAL_STATE.taskTimer) { clearInterval(GLOBAL_STATE.taskTimer); GLOBAL_STATE.taskTimer = null; } GLOBAL_STATE.isTaskRunning = false; const startBtn = document.getElementById('spark-task-start-btn'); startBtn.textContent = '启动'; startBtn.style.background = '#007aff'; startBtn.style.color = '#fff'; } function checkAndSend() { const { targetUsers, sendContent, sendTime } = GLOBAL_STATE.config; if (!GLOBAL_STATE.isTaskRunning) return; if (targetUsers.length === 0 || !sendContent || !validateTimeFormat(sendTime)) { stopTask(); return; } if (!isReachSendTime(sendTime)) { return; } const allSend = targetUsers.every(user => GLOBAL_STATE.todaySendRecord[user]?.isSuccess); if (allSend) { return; } batchSendMessage(targetUsers, sendContent); } function initTodaySendRecord() { const recordStr = localStorage.getItem(STORAGE_KEYS.SEND_RECORD); if (!recordStr) { GLOBAL_STATE.todaySendRecord = {}; return; } const record = JSON.parse(recordStr); const nowDate = new Date().toLocaleDateString(); for (const userName in record) { const sendDate = new Date(record[userName].sendTime).toLocaleDateString(); if (sendDate === nowDate) { GLOBAL_STATE.todaySendRecord[userName] = record[userName]; } } updateSparkStatusPanel(); } function crossDayReset() { GLOBAL_STATE.currentDate = new Date().toLocaleDateString(); GLOBAL_STATE.todaySendRecord = {}; localStorage.removeItem(STORAGE_KEYS.SEND_RECORD); updateSparkStatusPanel(); scheduleNextDayReset(); } function scheduleNextDayReset() { if (GLOBAL_STATE.dayResetTimer) { clearTimeout(GLOBAL_STATE.dayResetTimer); GLOBAL_STATE.dayResetTimer = null; } const now = new Date(); const nextReset = new Date(now); nextReset.setDate(now.getDate() + 1); nextReset.setHours(0, 0, 0, 0); const timeUntilReset = nextReset.getTime() - now.getTime(); const delay = timeUntilReset > 0 ? timeUntilReset : 60 * 1000; GLOBAL_STATE.dayResetTimer = setTimeout(() => { crossDayReset(); }, delay); } function startDayResetTimer() { scheduleNextDayReset(); } function updateSparkStatusPanel() { const statusContainer = document.getElementById('spark-status-container'); if (!statusContainer) return; const { targetUsers } = GLOBAL_STATE.config; if (targetUsers.length === 0) { statusContainer.innerHTML = '