当前位置: 首页 > news >正文

抖音火花任务自动化脚本

本文创建一个完整的抖音火花任务自动化脚本,从技术到核心实现,全面掌握浏览器自动化、DOM操作、数据持久化等前端技术。

🎯 项目概述

这是一个基于 Tampermonkey 的浏览器用户脚本,用于自动化完成抖音火花任务。该脚本实现了定时发送消息、智能重试、多API集成、日志管理等完整功能,是一个典型的前端自动化解决方案。
在这里插入图片描述

主要特性

  • 定时任务调度:精确到秒的定时发送机制
  • 🔄 智能重试机制:可配置的失败重试策略
  • 🎨 可视化控制面板:完整的UI交互界面
  • 💾 数据持久化:基于GM_setValue的本地存储
  • 🔍 动态DOM监听:MutationObserver实时监控页面变化
  • 📊 日志系统:完整的操作记录与导出功能

🛠️ 技术栈

1. Tampermonkey API

Tampermonkey 提供了强大的浏览器扩展能力,本项目主要使用了以下API:

// 数据持久化
GM_getValue(key, defaultValue)  // 读取存储数据
GM_setValue(key, value)          // 保存数据// 跨域HTTP请求
GM_xmlhttpRequest({method: 'GET',url: 'https://api.example.com',responseType: 'json',onload: function(response) { }
})// 系统通知
GM_notification({title: '标题',text: '内容',timeout: 3000
})

技术要点

  • GM_getValue/GM_setValue 提供了跨页面的数据持久化能力,数据存储在浏览器本地
  • GM_xmlhttpRequest 可以绕过CORS限制,实现跨域请求
  • 这些API是用户脚本的核心能力,使得脚本可以突破普通网页的沙箱限制

2. MutationObserver API

用于监听DOM树的变化,这是实现动态页面自动化的关键技术。

// 创建观察器实例
const observer = new MutationObserver((mutations) => {for (let mutation of mutations) {if (mutation.addedNodes.length > 0) {// 处理新增节点handleNewNodes(mutation.addedNodes);}}
});// 配置并启动观察器
observer.observe(document.body, {childList: true,    // 监听子节点变化subtree: true,      // 监听所有后代节点attributes: false   // 不监听属性变化
});// 停止观察
observer.disconnect();

技术深度解析

MutationObserver 是现代浏览器提供的异步观察DOM变化的API,相比传统的轮询方式具有以下优势:

  1. 性能优越:异步批量处理变化,不会阻塞主线程
  2. 精确捕获:可以准确捕获DOM的各种变化类型
  3. 灵活配置:支持细粒度的观察配置

配置选项详解

  • childList: 监听目标节点的子节点增删
  • subtree: 监听目标节点及其所有后代节点
  • attributes: 监听属性变化
  • characterData: 监听文本内容变化
  • attributeOldValue: 记录属性的旧值
  • characterDataOldValue: 记录文本的旧值

3. 定时任务系统

// 倒计时更新
countdownInterval = setInterval(() => {const now = new Date();const diff = targetTime - now;if (diff <= 0) {// 触发发送任务sendMessage();clearInterval(countdownInterval);} else {// 更新倒计时显示updateCountdown(diff);}
}, 1000);

时间处理技巧

// 解析时间字符串为日期对象
function parseTimeString(timeStr) {const [hours, minutes, seconds] = timeStr.split(':').map(Number);const now = new Date();const targetTime = new Date(now);targetTime.setHours(hours, minutes, seconds || 0, 0);// 如果目标时间已经过去,设置为明天if (targetTime <= now) {targetTime.setDate(targetTime.getDate() + 1);}return targetTime;
}

🏗️ 架构设计

整体架构图

┌─────────────────────────────────────────────────────────┐
│                    用户界面层 (UI Layer)                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  控制面板    │  │  设置面板    │  │  历史日志    │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────┘↓
┌─────────────────────────────────────────────────────────┐
│                   业务逻辑层 (Logic Layer)                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  定时调度    │  │  消息发送    │  │  重试机制    │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  DOM监听     │  │  目标查找    │  │  日志管理    │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────┘↓
┌─────────────────────────────────────────────────────────┐
│                   数据层 (Data Layer)                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  配置管理    │  │  状态存储    │  │  API集成     │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────┘

数据流设计

// 配置初始化流程
initConfig()loadFromStorage()mergeWithDefaults()saveConfig()// 消息发送流程
sendMessage()findTargetUser()waitForChatInput()getMessageContent()sendToInput()clickSendButton()verifySuccess()updateStatus()// DOM监听流程
initObserver()observeChanges()detectTarget()triggerAction()stopObserver()

💡 核心功能实现

1. 配置管理

采用默认配置 + 用户配置合并的策略,确保配置的完整性和向后兼容性。

// 默认配置定义
const DEFAULT_CONFIG = {baseMessage: "续火",sendTime: "00:01:00",checkInterval: 1000,maxWaitTime: 30000,maxRetryCount: 3,hitokotoTimeout: 60000,txtApiTimeout: 60000,useHitokoto: true,useTxtApi: true,txtApiMode: "manual",txtApiManualRandom: true,customMessage: "—————续火—————\n\n[TXTAPI]\n\n—————每日一言—————\n\n[API]\n",hitokotoFormat: "{hitokoto}\n—— {from}{from_who}",fromFormat: "{from}",fromWhoFormat: "「{from_who}」",txtApiUrl: "https://xxx/?encode=text",txtApiManualText: "文本1\n文本2\n文本3",targetUsername: "请修改此处为续火目标用户名",maxLogCount: 200
};// 配置初始化
function initConfig() {const savedConfig = GM_getValue('userConfig');userConfig = savedConfig ? {...DEFAULT_CONFIG, ...savedConfig} : {...DEFAULT_CONFIG};// 确保所有配置项都存在(向后兼容)for (const key in DEFAULT_CONFIG) {if (userConfig[key] === undefined) {userConfig[key] = DEFAULT_CONFIG[key];}}GM_setValue('userConfig', userConfig);return userConfig;
}

设计亮点

  • 对象展开运算符:使用 {...DEFAULT_CONFIG, ...savedConfig} 实现配置合并
  • 向后兼容:遍历检查确保所有配置项都存在
  • 热更新支持:配置修改后立即生效并持久化

模块化设计

// 配置模块封装
const ConfigModule = {init: initConfig,save: saveConfig,reset: resetAllConfig,get: (key) => userConfig[key],set: (key, value) => {userConfig[key] = value;GM_setValue('userConfig', userConfig);}
};// 使用常量管理配置键
const CONFIG_KEYS = {BASE_MESSAGE: 'baseMessage',SEND_TIME: 'sendTime',MAX_RETRY: 'maxRetryCount',TARGET_USER: 'targetUsername'
};

2. 智能DOM查找机制

针对动态加载的页面,实现了多策略的元素查找机制。

function tryClickTargetUser() {if (hasClickedTarget) return true;try {// 多选择器策略const selectors = ['[class*="name"]','[class*="username"]','[class*="header"]','[class*="item"]','[class*="contact"]','[class*="list"] [class*="name"]','[class*="chat"] [class*="name"]','[class*="message"] [class*="name"]'];// 遍历所有选择器for (let selector of selectors) {const elements = document.querySelectorAll(selector);for (let element of elements) {if (element.textContent && element.textContent.trim() === userConfig.targetUsername) {element.click();hasClickedTarget = true;updateChatTargetStatus('已找到', true);return true;}}}return false;} catch (error) {addLog(`寻找目标用户时出错: ${error.message}`, 'error');return false;}
}

技术要点

  • 模糊匹配:使用 [class*="name"] 匹配包含特定字符串的class
  • 多层级查找:支持嵌套选择器如 [class*="list"] [class*="name"]
  • 文本精确匹配:通过 textContent.trim() 确保精确匹配
  • 容错处理:try-catch 包裹,避免单个查找失败影响整体流程

选择器优化策略

// 使用常量管理选择器
const SELECTORS = {CHAT_INPUT: '.chat-input-dccKiL',SEND_BUTTON: '.chat-btn',USER_NAME: '[class*="name"]',MESSAGE_LIST: '[class*="list"] [class*="name"]'
};// 优化查询性能
function findElement(selector, parent = document) {// 缓存查询结果if (!this.elementCache) this.elementCache = new Map();const cacheKey = `${selector}_${parent}`;if (this.elementCache.has(cacheKey)) {const cached = this.elementCache.get(cacheKey);if (document.contains(cached)) return cached;}const element = parent.querySelector(selector);if (element) this.elementCache.set(cacheKey, element);return element;
}

3. 消息内容组装系统

支持多种API集成和自定义格式化。

async function getMessageContent() {let customMessage = userConfig.customMessage || userConfig.baseMessage;// 获取一言内容let hitokotoContent = '';if (userConfig.useHitokoto) {try {addLog('正在获取一言内容...', 'info');hitokotoContent = await getHitokoto();addLog('一言内容获取成功', 'success');} catch (error) {addLog(`一言获取失败: ${error.message}`, 'error');hitokotoContent = '一言获取失败~';}}// 获取TXTAPI内容let txtApiContent = '';if (userConfig.useTxtApi) {try {addLog('正在获取TXTAPI内容...', 'info');txtApiContent = await getTxtApiContent();addLog('TXTAPI内容获取成功', 'success');} catch (error) {addLog(`TXTAPI获取失败: ${error.message}`, 'error');txtApiContent = 'TXTAPI获取失败~';}}// 替换占位符if (customMessage.includes('[API]')) {customMessage = customMessage.replace('[API]', hitokotoContent);} else if (userConfig.useHitokoto) {customMessage += ` | ${hitokotoContent}`;}if (customMessage.includes('[TXTAPI]')) {customMessage = customMessage.replace('[TXTAPI]', txtApiContent);} else if (userConfig.useTxtApi) {customMessage += ` | ${txtApiContent}`;}return customMessage;
}

一言API格式化实现

function formatHitokoto(format, data) {let result = format.replace(/{hitokoto}/g, data.hitokoto || '');// 条件格式化fromlet fromFormatted = '';if (data.from) {fromFormatted = userConfig.fromFormat.replace(/{from}/g, data.from);}result = result.replace(/{from}/g, fromFormatted);// 条件格式化from_wholet fromWhoFormatted = '';if (data.from_who) {fromWhoFormatted = userConfig.fromWhoFormat.replace(/{from_who}/g, data.from_who);}result = result.replace(/{from_who}/g, fromWhoFormatted);return result;
}

设计

  • 异步并发:使用 async/await 处理多个API请求
  • 占位符系统:灵活的内容替换机制
  • 条件格式化:根据数据是否存在决定是否显示格式
  • 降级策略:API失败时使用默认文本

Promise与async/await最佳实践

// 方式1:Promise链式调用
function getMessageContentPromise() {return getHitokoto().then(hitokoto => {return getTxtApiContent().then(txtApi => ({ hitokoto, txtApi }));}).then(({ hitokoto, txtApi }) => {return formatMessage(hitokoto, txtApi);}).catch(error => {console.error('获取消息内容失败:', error);return userConfig.baseMessage;});
}// 方式2:async/await(推荐)
async function getMessageContentAsync() {try {// 并发请求提升性能const [hitokoto, txtApi] = await Promise.all([getHitokoto().catch(() => ''),getTxtApiContent().catch(() => '')]);return formatMessage(hitokoto, txtApi);} catch (error) {console.error('获取消息内容失败:', error);return userConfig.baseMessage;}
}// 方式3:带超时控制的Promise
function withTimeout(promise, timeoutMs) {return Promise.race([promise,new Promise((_, reject) => setTimeout(() => reject(new Error('请求超时')), timeoutMs))]);
}// 使用示例
const hitokoto = await withTimeout(getHitokoto(), 5000);

4. TXTAPI双模式

支持API模式和手动模式,手动模式还支持顺序/随机两种策略。

function getTxtApiContent() {return new Promise((resolve, reject) => {if (userConfig.txtApiMode === 'api') {// API模式:发送HTTP请求const timeout = setTimeout(() => {reject(new Error('TXTAPI请求超时'));}, userConfig.txtApiTimeout);GM_xmlhttpRequest({method: 'GET',url: userConfig.txtApiUrl,onload: function(response) {clearTimeout(timeout);if (response.status === 200) {updateTxtApiStatus('获取成功');resolve(response.responseText.trim());} else {updateTxtApiStatus('请求失败', false);reject(new Error(`TXTAPI请求失败: ${response.status}`));}},onerror: function(error) {clearTimeout(timeout);updateTxtApiStatus('网络错误', false);reject(new Error('TXTAPI网络错误'));}});} else {// 手动模式:从配置文本中选择const lines = userConfig.txtApiManualText.split('\n').filter(line => line.trim());if (lines.length === 0) {updateTxtApiStatus('无内容', false);reject(new Error('手动文本内容为空'));return;}let sentIndexes = GM_getValue('txtApiManualSentIndexes', []);if (userConfig.txtApiManualRandom) {// 随机模式let availableIndexes = [];for (let i = 0; i < lines.length; i++) {if (!sentIndexes.includes(i)) {availableIndexes.push(i);}}// 所有行都已发送,重置记录if (availableIndexes.length === 0) {sentIndexes = [];availableIndexes = Array.from({length: lines.length}, (_, i) => i);GM_setValue('txtApiManualSentIndexes', []);}// 随机选择const randomIndex = Math.floor(Math.random() * availableIndexes.length);const selectedIndex = availableIndexes[randomIndex];const selectedText = lines[selectedIndex].trim();sentIndexes.push(selectedIndex);GM_setValue('txtApiManualSentIndexes', sentIndexes);updateTxtApiStatus('获取成功');resolve(selectedText);} else {// 顺序模式let nextIndex = 0;if (sentIndexes.length > 0) {nextIndex = (sentIndexes[sentIndexes.length - 1] + 1) % lines.length;}const selectedText = lines[nextIndex].trim();sentIndexes.push(nextIndex);GM_setValue('txtApiManualSentIndexes', sentIndexes);updateTxtApiStatus('获取成功');resolve(selectedText);}}});
}

技术

  • 双模式设计:API模式和手动模式互不干扰
  • 去重机制:记录已发送的索引,避免重复
  • 自动重置:所有内容发送完后自动重置
  • 随机算法:从未发送的内容中随机选择

随机算法优化

// Fisher-Yates洗牌算法(更均匀的随机分布)
function shuffleArray(array) {const result = [...array];for (let i = result.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));[result[i], result[j]] = [result[j], result[i]];}return result;
}// 改进的随机选择(预先洗牌,顺序消费)
function getTxtApiContentImproved() {let shuffledIndexes = GM_getValue('txtApiShuffledIndexes', []);let currentIndex = GM_getValue('txtApiCurrentIndex', 0);const lines = userConfig.txtApiManualText.split('\n').filter(line => line.trim());// 需要重新洗牌if (shuffledIndexes.length === 0 || currentIndex >= shuffledIndexes.length) {shuffledIndexes = shuffleArray(Array.from({length: lines.length}, (_, i) => i));currentIndex = 0;GM_setValue('txtApiShuffledIndexes', shuffledIndexes);}const selectedIndex = shuffledIndexes[currentIndex];const selectedText = lines[selectedIndex].trim();GM_setValue('txtApiCurrentIndex', currentIndex + 1);return Promise.resolve(selectedText);
}

5. 重试机制

async function executeSendProcess() {retryCount++;updateRetryCount();if (retryCount > userConfig.maxRetryCount) {addLog(`已达到最大重试次数 (${userConfig.maxRetryCount})`, 'error');isProcessing = false;return;}addLog(`尝试发送 (${retryCount}/${userConfig.maxRetryCount})`, 'info');if (!hasClickedTarget) {addLog('目标用户未找到,先查找目标用户...', 'info');findAndClickTargetUser();} else {addLog('目标用户已找到,直接查找聊天输入框...', 'info');setTimeout(tryFindChatInput, 1000);}
}

重试策略

  • 计数器控制:通过 retryCount 控制重试次数
  • 状态检查:根据 hasClickedTarget 决定重试步骤
  • 延迟重试:使用 setTimeout 避免频繁重试
  • 失败退出:达到最大次数后停止处理

6. 日志系统

完整的日志记录、展示和导出功能。

function addLog(message, type = 'info') {const now = new Date();const timeString = now.toLocaleTimeString();const logEntry = {time: timeString,message: message,type: type};// 添加到内存allLogs.push(logEntry);// 限制日志数量if (allLogs.length > userConfig.maxLogCount) {allLogs = allLogs.slice(-userConfig.maxLogCount);}// 持久化存储GM_setValue('allLogs', allLogs);// UI显示const logEntryElement = document.createElement('div');logEntryElement.style.color = type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : type === 'warning' ? '#ffc107' : '#17a2b8';logEntryElement.textContent = `${timeString} - ${message}`;const logContainer = document.getElementById('dy-fire-log');if (logContainer) {logContainer.prepend(logEntryElement);// 限制显示数量if (logContainer.children.length > 8) {logContainer.removeChild(logContainer.lastChild);}logContainer.scrollTop = 0;}
}

日志导出

document.getElementById('dy-fire-history-export').addEventListener('click', function() {if (logs.length === 0) {alert('没有日志可导出');return;}const logText = logs.map(log => `${log.time} - ${log.message}`).join('\n');const blob = new Blob([logText], { type: 'text/plain' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `抖音续火助手日志_${new Date().toISOString().slice(0, 10)}.txt`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);addLog('日志已导出', 'success');
});

技术

  • 分级日志:支持 info、success、error、warning 四种级别
  • 双重存储:内存 + 持久化存储
  • 数量限制:防止日志无限增长
  • Blob导出:使用 Blob API 实现文件下载

日志模块化设计

// 日志模块封装
const LogModule = {// 日志类型常量TYPES: {INFO: 'info',SUCCESS: 'success',ERROR: 'error',WARNING: 'warning'},// 日志颜色映射COLORS: {info: '#17a2b8',success: '#28a745',error: '#dc3545',warning: '#ffc107'},// 添加日志add: addLog,// 导出日志export: function() {const logs = GM_getValue('allLogs', []);if (logs.length === 0) {alert('没有日志可导出');return;}const logText = logs.map(log => `${log.time} [${log.type.toUpperCase()}] ${log.message}`).join('\n');const blob = new Blob([logText], { type: 'text/plain;charset=utf-8' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `抖音续火助手日志_${new Date().toISOString().slice(0, 10)}.txt`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);this.add('日志已导出', this.TYPES.SUCCESS);},// 清空日志clear: function() {GM_setValue('allLogs', []);const logContainer = document.getElementById('dy-fire-log');if (logContainer) {logContainer.innerHTML = '';}this.add('日志已清空', this.TYPES.INFO);},// 获取日志统计getStats: function() {const logs = GM_getValue('allLogs', []);return {total: logs.length,info: logs.filter(l => l.type === this.TYPES.INFO).length,success: logs.filter(l => l.type === this.TYPES.SUCCESS).length,error: logs.filter(l => l.type === this.TYPES.ERROR).length,warning: logs.filter(l => l.type === this.TYPES.WARNING).length};}
};

🔥 难点实现

难点1:动态页面元素查找

问题:现代Web应用大量使用动态加载,元素可能在任意时刻出现。

解决方案

function findAndClickTargetUser() {addLog(`开始查找目标用户: ${userConfig.targetUsername}`, 'info');// 停止之前的观察器if (sendProcessObserver) {sendProcessObserver.disconnect();sendProcessObserver = null;}// 先立即尝试一次if (tryClickTargetUser()) {return;}// 启动观察器监听DOM变化sendProcessObserver = new MutationObserver((mutations) => {if (hasClickedTarget) {sendProcessObserver.disconnect();return;}for (let mutation of mutations) {if (mutation.addedNodes.length > 0) {if (tryClickTargetUser()) {sendProcessObserver.disconnect();return;}}}});// 观察整个文档sendProcessObserver.observe(document.body, {childList: true,subtree: true});// 设置超时setTimeout(() => {if (!hasClickedTarget && sendProcessObserver) {sendProcessObserver.disconnect();addLog('查找目标用户超时,重试中...', 'warning');setTimeout(executeSendProcess, 2000);}}, 10000);
}

关键

  1. 立即尝试 + 观察器:先尝试直接查找,失败后启动观察器
  2. 状态标记:使用 hasClickedTarget 避免重复点击
  3. 超时机制:防止无限等待
  4. 观察器清理:及时断开观察器释放资源

观察器性能

// 防抖优化:避免频繁触发
function createDebouncedObserver(callback, delay = 100) {let debounceTimer = null;return new MutationObserver((mutations) => {clearTimeout(debounceTimer);debounceTimer = setTimeout(() => {callback(mutations);}, delay);});
}// 节流优化:限制执行频率
function createThrottledObserver(callback, delay = 100) {let lastTime = 0;return new MutationObserver((mutations) => {const now = Date.now();if (now - lastTime >= delay) {callback(mutations);lastTime = now;}});
}// 使用示例
const debouncedObserver = createDebouncedObserver((mutations) => {tryClickTargetUser();
}, 200);debouncedObserver.observe(document.body, {childList: true,subtree: true
});

难点2:换行符处理

问题:直接设置 textContentvalue 无法正确处理换行符。

解决方案

// 清空输入框
input.textContent = '';
input.focus();// 处理换行符
const lines = messageToSend.split('\n');
for (let i = 0; i < lines.length; i++) {document.execCommand('insertText', false, lines[i]);if (i < lines.length - 1) {// 插入换行符document.execCommand('insertLineBreak');}
}input.dispatchEvent(new Event('input', { bubbles: true }));

技术要点

  • 使用 document.execCommand('insertText') 插入文本
  • 使用 document.execCommand('insertLineBreak') 插入换行
  • 触发 input 事件通知框架更新

事件模拟最佳实践

// 完整的事件模拟流程
function simulateUserInput(element, text) {// 1. 聚焦元素element.focus();// 2. 触发焦点事件element.dispatchEvent(new FocusEvent('focus', { bubbles: true }));// 3. 处理换行符const lines = text.split('\n');for (let i = 0; i < lines.length; i++) {// 插入文本document.execCommand('insertText', false, lines[i]);// 插入换行(除了最后一行)if (i < lines.length - 1) {document.execCommand('insertLineBreak');}}// 4. 触发输入事件element.dispatchEvent(new Event('input', { bubbles: true }));element.dispatchEvent(new Event('change', { bubbles: true }));// 5. 触发键盘事件(某些框架需要)element.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }));element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
}// 使用事件委托优化
function setupEventDelegation(container, selector, eventType, handler) {container.addEventListener(eventType, (e) => {if (e.target.matches(selector)) {handler(e);}});
}// 示例:为所有按钮添加点击事件
setupEventDelegation(document.body, 'button.send-btn', 'click', (e) => {console.log('发送按钮被点击');
});

难点3:跨域API请求

问题:浏览器的同源策略限制了跨域请求。

解决方案

GM_xmlhttpRequest({method: 'GET',url: 'https://v1.hitokoto.cn/',responseType: 'json',timeout: userConfig.hitokotoTimeout,onload: function(response) {if (response.status === 200) {const data = response.response;resolve(formatHitokoto(userConfig.hitokotoFormat, data));}},onerror: function(error) {reject(new Error('API网络错误'));},ontimeout: function() {reject(new Error('API请求超时'));}
});

优势

  • 绕过CORS限制
  • 支持设置超时
  • 完整的错误处理

难点4:时间计算与跨天处理

问题:需要正确处理跨天的时间计算。

解决方案

function parseTimeString(timeStr) {const [hours, minutes, seconds] = timeStr.split(':').map(Number);const now = new Date();const targetTime = new Date(now);targetTime.setHours(hours, minutes, seconds || 0, 0);// 如果目标时间已经过去,设置为明天if (targetTime <= now) {targetTime.setDate(targetTime.getDate() + 1);}return targetTime;
}// 倒计时更新
function startCountdown(targetTime) {function update() {const now = new Date();const diff = targetTime - now;if (diff <= 0) {// 检查今天是否已发送const lastSentDate = GM_getValue('lastSentDate', '');const today = new Date().toDateString();if (lastSentDate === today) {// 已发送,设置明天的目标时间nextSendTime = parseTimeString(userConfig.sendTime);startCountdown(nextSendTime);} else {// 未发送,执行发送sendMessage();}return;}// 更新倒计时显示const hours = Math.floor(diff / (1000 * 60 * 60));const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));const seconds = Math.floor((diff % (1000 * 60)) / 1000);updateCountdownDisplay(hours, minutes, seconds);}update();countdownInterval = setInterval(update, 1000);
}

关键点

  • 使用 toDateString() 比较日期,忽略时间部分
  • 自动处理跨天情况
  • 倒计时归零后检查发送状态

🔒 安全性

1. 输入验证

// 时间格式验证
function validateTimeFormat(timeValue) {const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/;if (!timeRegex.test(timeValue)) {addLog('时间格式错误,请使用HH:mm:ss格式', 'error');return false;}return true;
}// 数值范围验证
function validateRetryCount(maxRetryCount) {if (isNaN(maxRetryCount) || maxRetryCount < 1 || maxRetryCount > 10) {addLog('重试次数必须是1-10之间的数字', 'error');return false;}return true;
}// URL验证
function validateUrl(url) {try {new URL(url);return true;} catch (error) {addLog('URL格式错误', 'error');return false;}
}// 综合验证器
const Validator = {time: validateTimeFormat,number: (value, min, max) => !isNaN(value) && value >= min && value <= max,url: validateUrl,notEmpty: (value) => value && value.trim().length > 0
};

2. XSS防护

// 使用textContent而非innerHTML
logEntryElement.textContent = `${timeString} - ${message}`;// 创建元素而非字符串拼接
const element = document.createElement('div');
element.textContent = userInput;  // 自动转义// HTML转义函数
function escapeHtml(unsafe) {return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}// 安全的innerHTML设置
function safeSetInnerHTML(element, html) {element.textContent = '';  // 清空const sanitized = escapeHtml(html);element.innerHTML = sanitized;
}

3. 错误处理

// 统一错误处理
function handleError(error, context = '') {const errorMessage = `${context ? context + ': ' : ''}${error.message}`;addLog(errorMessage, 'error');// 错误上报(可选)if (userConfig.enableErrorReport) {reportError(error, context);}
}// Try-Catch包装器
function tryCatch(fn, fallback, context = '') {try {return fn();} catch (error) {handleError(error, context);return fallback ? fallback() : null;}
}// 异步错误处理
async function asyncTryCatch(fn, fallback, context = '') {try {return await fn();} catch (error) {handleError(error, context);return fallback ? await fallback() : null;}
}// 使用示例
const result = tryCatch(() => riskyOperation(),() => defaultValue,'执行风险操作'
);

4. 资源清理

// 资源管理器
class ResourceManager {constructor() {this.observers = [];this.timers = [];this.eventListeners = [];}addObserver(observer) {this.observers.push(observer);return observer;}addTimer(timer) {this.timers.push(timer);return timer;}addEventListener(element, event, handler) {element.addEventListener(event, handler);this.eventListeners.push({ element, event, handler });}cleanup() {// 断开所有观察器this.observers.forEach(observer => observer.disconnect());this.observers = [];// 清除所有定时器this.timers.forEach(timer => clearInterval(timer));this.timers = [];// 移除所有事件监听器this.eventListeners.forEach(({ element, event, handler }) => {element.removeEventListener(event, handler);});this.eventListeners = [];}
}// 使用示例
const resourceManager = new ResourceManager();// 页面卸载时清理资源
window.addEventListener('beforeunload', () => {resourceManager.cleanup();
});

📈 应用场景

1. 自动签到脚本

// 每日自动签到
async function autoCheckIn() {const checkInButton = await waitForElement('.check-in-btn');checkInButton.click();const result = await waitForElement('.check-in-result');if (result.textContent.includes('成功')) {GM_setValue('lastCheckInDate', new Date().toDateString());GM_notification({title: '签到成功',text: '今日签到已完成'});}
}// 等待元素出现
function waitForElement(selector, timeout = 10000) {return new Promise((resolve, reject) => {const element = document.querySelector(selector);if (element) {resolve(element);return;}const observer = new MutationObserver(() => {const element = document.querySelector(selector);if (element) {observer.disconnect();resolve(element);}});observer.observe(document.body, {childList: true,subtree: true});setTimeout(() => {observer.disconnect();reject(new Error('等待元素超时'));}, timeout);});
}

2. 表单自动填充

// 智能表单填充
function autoFillForm(formData) {Object.keys(formData).forEach(key => {const input = document.querySelector(`[name="${key}"]`);if (input) {// 根据输入类型选择填充方式if (input.type === 'checkbox' || input.type === 'radio') {input.checked = formData[key];} else if (input.tagName === 'SELECT') {input.value = formData[key];} else {input.value = formData[key];}// 触发必要的事件input.dispatchEvent(new Event('input', { bubbles: true }));input.dispatchEvent(new Event('change', { bubbles: true }));}});
}// 使用示例
const userData = {username: '张三',email: 'zhangsan@example.com',age: '25',gender: 'male',agree: true
};autoFillForm(userData);

3. 页面监控与通知

// 页面内容监控
class PageMonitor {constructor(keywords, callback) {this.keywords = keywords;this.callback = callback;this.observer = null;}start() {this.observer = new MutationObserver((mutations) => {mutations.forEach(mutation => {if (mutation.addedNodes.length > 0) {const newContent = Array.from(mutation.addedNodes).filter(node => node.nodeType === 1).map(node => node.textContent);const matchedKeywords = this.keywords.filter(keyword => newContent.some(text => text.includes(keyword)));if (matchedKeywords.length > 0) {this.callback(matchedKeywords, newContent);}}});});this.observer.observe(document.body, {childList: true,subtree: true});}stop() {if (this.observer) {this.observer.disconnect();}}
}// 使用示例
const monitor = new PageMonitor(['优惠', '折扣', '限时'], (keywords, content) => {GM_notification({title: '检测到关键内容',text: `发现关键词: ${keywords.join(', ')}`});
});monitor.start();

欢迎讨论~

本项目综合运用了以下核心技术:

  1. Tampermonkey API:实现跨域请求、数据持久化、系统通知
  2. MutationObserver:监听DOM变化,实现动态页面自动化
  3. Promise/async-await:优雅处理异步操作
  4. 定时任务:精确的时间调度和倒计时
  5. DOM操作:元素查找、事件模拟、内容插入
  6. 数据管理:配置系统、日志系统、状态管理
http://www.dtcms.com/a/549582.html

相关文章:

  • 从入门到实践:Linux 基础学习(xshell)
  • 《URP管线主导的角色材质、阴影与显存动态适配优化方案》
  • TensorFlow深度学习实战——自定义图数据集
  • Flutter 3.29.0 使用RepaintBoundary或者ScreenshotController出现导出图片渲染上下颠倒问题
  • Flutter---个人信息(4)---实现修改生日日期
  • 不止于加热:管式炉在材料科学与新能源研发中的关键作用
  • 深圳网站建设方案优化深圳发布广告的平台有哪些
  • Go语言中json.RawMessage
  • Pytorch常用函数学习摘录
  • 个人什么取消网站备案铭万做的网站怎么样
  • 2025-10-30 ZYZOJ Star(斯达)模拟赛 hetao1733837的record
  • 百胜中台×OceanBase:打造品牌零售降本增效的数字核心引擎,热门服饰、美妆客户已实践
  • 深度学习调试工具链:从PyTorch Profiler到TensorBoard可视化
  • 不可变借用的规则与限制: 从只读语义到零拷贝架构的 5 000 字深潜
  • 专题三 之 【二分查找】
  • C++进阶: override和final说明符-----继承2中重写的确认官和刹车(制动器)
  • 数据科学每日总结--Day7--数据库
  • opencv 学习: 01 ubuntu20.04 下 opencv 4.12.0 源码编译
  • 满足“国六”标准的通用型故障诊断仪:Q-OBD
  • 上海专业建站公湖南网站建设设计
  • 智慧时空大数据平台:释放时空信息数据价值
  • 线程基本概念
  • MySQL MDL锁阻塞DDL 导致复制线程卡住
  • 智慧管理,赋能美容院新未来
  • Flink做checkpoint迟迟过不去的临时解决思路
  • 网站注册 优帮云wordpress首页静态化
  • [人工智能-大模型-115]:模型层 - 用通俗易懂的语言,阐述神经网络为啥需要多层
  • Actix Web 不是 Nginx:解析 Rust 应用服务器与传统 Web 服务器的本质区别
  • pdf文件上传下载记录
  • 辽阳网站设计中国建设银行的网站.