1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/Bg6shY若失效,可用地址
阿里云:
服务器购买地址
https://t.aliyun.com/U/Bg6shY若失效,可用地址
https://www.aliyun.com/daily-act/ecs/activity_selection?source=5176.29345612&userCode=49hts92d腾讯云:
https://curl.qcloud.com/wJpWmSfU若失效,可用地址
https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ad201ee2ef3b771157f72ee5464b1fea&from=console华为云
https://activity.huaweicloud.com/cps.html?fromacct=64b5cf7cc11b4840bb4ed2ea0b2f4468&utm_source=V1g3MDY4NTY=&utm_medium=cps&utm_campaign=2019052.部署教程
3.代码如下
(function() {'use strict';// 默认配置const DEFAULT_CONFIG = {baseMessage: "续火",sendTime: "00:01:00",checkInterval: 1000,maxWaitTime: 30000,maxRetryCount: 3,hitokotoTimeout: 60000,txtApiTimeout: 60000,useHitokoto: true,useTxtApi: true, // 默认开启TXTAPItxtApiMode: "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 // 最大日志数量};// 状态变量let isProcessing = false;let retryCount = 0;let countdownInterval = null;let userConfig = {};let nextSendTime = null;let chatObserver = null; // 聊天列表观察器let hasClickedTarget = false; // 是否已点击目标用户let initialAttemptTimer = null; // 初始尝试定时器let debounceTimer = null; // 防抖定时器let allLogs = []; // 所有日志记录let sendProcessObserver = null; // 发送流程中的观察器// 初始化配置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];}}// 初始化手动文本发送记录if (!GM_getValue('txtApiManualSentIndexes')) {GM_setValue('txtApiManualSentIndexes', []);}// 初始化日志记录allLogs = GM_getValue('allLogs', []);GM_setValue('userConfig', userConfig);return userConfig;}// 保存配置function saveConfig() {GM_setValue('userConfig', userConfig);}// 创建UI控制面板function createControlPanel() {// 移除可能已存在的面板const existingPanel = document.getElementById('dy-fire-helper');if (existingPanel) {existingPanel.remove();}const panel = document.createElement('div');panel.id = 'dy-fire-helper';panel.style.cssText = `position: fixed;top: 20px;right: 20px;width: 400px;background: rgba(255, 255, 255, 0.98);border-radius: 12px;box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);z-index: 9999;font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;padding: 15px;color: #333;transition: all 0.3s ease;max-height: 500px;overflow: hidden;border: 1px solid #eee;`;panel.innerHTML = `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"><h3 style="margin: 0; color: #ff2c54; font-size: 16px; display: flex; align-items: center;"><span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background: #28a745; margin-right: 8px;"></span></h3><button id="dy-fire-helper-close" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #999;">×</button></div><div style="margin-bottom: 15px;"><div style="display: flex; justify-content: space-between; margin-bottom: 8px;"><span style="font-weight: 500;">今日状态:</span><span id="dy-fire-status" style="color: #28a745; font-weight: 600;">已发送</span></div><div style="display: flex; justify-content: space-between; margin-bottom: 8px;"><span style="font-weight: 500;">下次发送:</span><span id="dy-fire-next">2023-11-05 00:01:00</span></div><div style="display: flex; justify-content: space-between; margin-bottom: 8px;"><span style="font-weight: 500;">倒计时:</span><span id="dy-fire-countdown" style="color: #dc3545; font-weight: 700;">23:45:12</span></div><div style="display: flex; justify-content: space-between;"><span style="font-weight: 500;">重试次数:</span><span id="dy-fire-retry">0/${userConfig.maxRetryCount}</span></div><div style="display: flex; justify-content: between; margin-top: 8px;"><span style="font-weight: 500;">一言状态:</span><span id="dy-fire-hitokoto">未获取</span></div><div style="display: flex; justify-content: between; margin-top: 8px;"><span style="font-weight: 500;">TXTAPI状态:</span><span id="dy-fire-txtapi">未获取</span></div><div style="display: flex; justify-content: between; margin-top: 8px;"><span style="font-weight: 500;">聊天目标:</span><span id="dy-fire-chat-target">${hasClickedTarget ? '已找到' : '寻找中'}</span></div></div><div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 15px;"><button id="dy-fire-send" style="padding: 8px 12px; background: #007bff; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500;">立即发送续火消息</button><button id="dy-fire-settings" style="padding: 8px 12px; background: #6c757d; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500;">设置</button><div style="display: flex; gap: 10px;"><button id="dy-fire-history" style="flex: 1; padding: 8px 12px; background: #17a2b8; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500;">历史日志</button><button id="dy-fire-clear" style="flex: 1; padding: 8px 12px; background: #dc3545; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500;">清空记录</button></div><button id="dy-fire-reset" style="padding: 8px 12px; background: #ffc107; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500;">重置配置</button></div><div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px;"><div style="font-weight: 500; margin-bottom: 8px;">操作日志</div><div id="dy-fire-log" style="font-size: 12px; height: 100px; overflow-y: auto; line-height: 1.4;"><div style="color: #28a745;">系统已就绪,等待执行...</div></div></div>`;document.body.appendChild(panel);// 添加关闭按钮事件document.getElementById('dy-fire-helper-close').addEventListener('click', function() {panel.style.display = 'none';});// 添加按钮事件document.getElementById('dy-fire-send').addEventListener('click', sendMessage);document.getElementById('dy-fire-settings').addEventListener('click', showSettingsPanel);document.getElementById('dy-fire-history').addEventListener('click', showHistoryPanel);document.getElementById('dy-fire-clear').addEventListener('click', clearData);document.getElementById('dy-fire-reset').addEventListener('click', resetAllConfig);}// 显示设置面板function showSettingsPanel() {// 移除可能已存在的设置面板const existingSettings = document.getElementById('dy-fire-settings-panel');if (existingSettings) {existingSettings.remove();return;}const settingsPanel = document.createElement('div');settingsPanel.id = 'dy-fire-settings-panel';settingsPanel.style.cssText = `position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);max-width: 90vw;width: 500px;background: white;border-radius: 12px;box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);z-index: 10000;padding: 20px;font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;max-height: 90vh;overflow-y: auto;box-sizing: border-box;`;settingsPanel.innerHTML = `<h3 style="margin: 0 0 20px 0; color: #ff2c54; display: flex; justify-content: space-between; align-items: center;"><span>设置</span><button id="dy-fire-settings-close" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #999;">×</button></h3><div style="margin-bottom: 15px;"><label style="display: block; margin-bottom: 5px; font-weight: 500;">发送时间 (HH:mm:ss)</label><input type="text" id="dy-fire-settings-time" value="${userConfig.sendTime}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;" placeholder="例如: 00:01:00"></div><div style="margin-bottom: 15px;"><label style="display: block; margin-bottom: 5px; font-weight: 500;">目标用户名</label><input type="text" id="dy-fire-settings-target-username" value="${userConfig.targetUsername}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;" placeholder="要自动点击聊天的用户名"></div><div style="margin-bottom: 15px;"><label style="display: flex; align-items: center; cursor: pointer;"><input type="checkbox" id="dy-fire-settings-use-hitokoto" ${userConfig.useHitokoto ? 'checked' : ''} style="margin-right: 8px;">使用一言API</label></div><div style="margin-bottom: 15px;"><label style="display: flex; align-items: center; cursor: pointer;"><input type="checkbox" id="dy-fire-settings-use-txtapi" ${userConfig.useTxtApi ? 'checked' : ''} style="margin-right: 8px;">使用TXTAPI</label></div><div id="txt-api-mode-container" style="margin-bottom: 15px; ${userConfig.useTxtApi ? '' : 'display: none;'}"><label style="display: block; margin-bottom: 5px; font-weight: 500;">TXTAPI模式</label><div style="display: flex; gap: 15px;"><label style="display: flex; align-items: center; cursor: pointer;"><input type="radio" name="txt-api-mode" value="api" ${userConfig.txtApiMode === 'api' ? 'checked' : ''} style="margin-right: 5px;">API模式</label><label style="display: flex; align-items: center; cursor: pointer;"><input type="radio" name="txt-api-mode" value="manual" ${userConfig.txtApiMode === 'manual' ? 'checked' : ''} style="margin-right: 5px;">手动模式</label></div></div><div id="txt-api-url-container" style="margin-bottom: 15px; ${userConfig.useTxtApi && userConfig.txtApiMode === 'api' ? '' : 'display: none;'}"><label style="display: block; margin-bottom: 5px; font-weight: 500;">TXTAPI链接</label><input type="text" id="dy-fire-settings-txtapi-url" value="${userConfig.txtApiUrl}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;" placeholder="例如: https://123456.cn"></div><div id="txt-api-manual-container" style="margin-bottom: 15px; ${userConfig.useTxtApi && userConfig.txtApiMode === 'manual' ? '' : 'display: none;'}"><div style="margin-bottom: 10px;"><label style="display: flex; align-items: center; cursor: pointer;"><input type="checkbox" id="dy-fire-settings-txtapi-random" ${userConfig.txtApiManualRandom ? 'checked' : ''} style="margin-right: 8px;">随机选择文本</label></div><label style="display: block; margin-bottom: 5px; font-weight: 500;">手动文本内容(一行一个)</label><textarea id="dy-fire-settings-txtapi-manual" style="width: 100%; height: 100px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; box-sizing: border-box;">${userConfig.txtApiManualText}</textarea></div><div style="margin-bottom: 15px;"><label style="display: block; margin-bottom: 5px; font-weight: 500;">最大重试次数</label><input type="number" id="dy-fire-settings-retry-count" min="1" max="10" value="${userConfig.maxRetryCount}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;"></div><div style="margin-bottom: 15px;"><label style="display: block; margin-bottom: 5px; font-weight: 500;">一言API格式</label><textarea id="dy-fire-settings-hitokoto-format" style="width: 100%; height: 60px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; box-sizing: border-box;">${userConfig.hitokotoFormat}</textarea><div style="font-size: 12px; color: #666; margin-top: 5px;">可用变量: {hitokoto} {from} {from_who}<br>示例: {hitokoto} ------ {from}{from_who}</div></div><div style="margin-bottom: 15px;"><label style="display: block; margin-bottom: 5px; font-weight: 500;">from格式</label><input type="text" id="dy-fire-settings-from-format" value="${userConfig.fromFormat}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;" placeholder="例如: {from}"><div style="font-size: 12px; color: #666; margin-top: 5px;">当from不为空时显示此格式,为空时不显示</div></div><div style="margin-bottom: 15px;"><label style="display: block; margin-bottom: 5px; font-weight: 500;">from_who格式</label><input type="text" id="dy-fire-settings-from-who-format" value="${userConfig.fromWhoFormat}" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;" placeholder="例如: 「{from_who}」"><div style="font-size: 12px; color: #666; margin-top: 5px;">当from_who不为空时显示此格式,为空时不显示</div></div><div style="margin-bottom: 20px;"><label style="display: block; margin-bottom: 5px; font-weight: 500;">自定义消息内容</label><textarea id="dy-fire-settings-custom-message" style="width: 100%; height: 100px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; box-sizing: border-box;">${userConfig.customMessage}</textarea><div style="font-size: 12px; color: #666; margin-top: 5px;">使用 [API] 作为一言内容的占位符<br>使用 [TXTAPI] 作为TXTAPI内容的占位符<br>支持换行符,关闭API时占位符标记将保留</div></div><button id="dy-fire-settings-save" style="width: 100%; padding: 10px; background: #28a745; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; box-sizing: border-box;">保存设置</button>`;document.body.appendChild(settingsPanel);// 添加TXTAPI模式切换事件const modeRadios = document.querySelectorAll('input[name="txt-api-mode"]');modeRadios.forEach(radio => {radio.addEventListener('change', function() {const mode = this.value;document.getElementById('txt-api-url-container').style.display = mode === 'api' ? 'block' : 'none';document.getElementById('txt-api-manual-container').style.display = mode === 'manual' ? 'block' : 'none';});});// 添加TXTAPI开关切换事件document.getElementById('dy-fire-settings-use-txtapi').addEventListener('change', function() {const useTxtApi = this.checked;document.getElementById('txt-api-mode-container').style.display = useTxtApi ? 'block' : 'none';// 根据当前选择的模式显示相应的表单const currentMode = document.querySelector('input[name="txt-api-mode"]:checked').value;document.getElementById('txt-api-url-container').style.display = (useTxtApi && currentMode === 'api') ? 'block' : 'none';document.getElementById('txt-api-manual-container').style.display = (useTxtApi && currentMode === 'manual') ? 'block' : 'none';});// 添加关闭按钮事件document.getElementById('dy-fire-settings-close').addEventListener('click', function() {settingsPanel.remove();});// 添加保存按钮事件document.getElementById('dy-fire-settings-save').addEventListener('click', function() {const timeValue = document.getElementById('dy-fire-settings-time').value;const targetUsername = document.getElementById('dy-fire-settings-target-username').value;const useHitokoto = document.getElementById('dy-fire-settings-use-hitokoto').checked;const useTxtApi = document.getElementById('dy-fire-settings-use-txtapi').checked;const txtApiMode = document.querySelector('input[name="txt-api-mode"]:checked').value;const txtApiRandom = document.getElementById('dy-fire-settings-txtapi-random').checked;const txtApiUrl = document.getElementById('dy-fire-settings-txtapi-url').value;const txtApiManualText = document.getElementById('dy-fire-settings-txtapi-manual').value;const maxRetryCount = parseInt(document.getElementById('dy-fire-settings-retry-count').value, 10);const hitokotoFormat = document.getElementById('dy-fire-settings-hitokoto-format').value;const fromFormat = document.getElementById('dy-fire-settings-from-format').value;const fromWhoFormat = document.getElementById('dy-fire-settings-from-who-format').value;const customMessage = document.getElementById('dy-fire-settings-custom-message').value;// 验证时间格式 (HH:mm:ss)if (!/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/.test(timeValue)) {addLog('时间格式错误,请使用HH:mm:ss格式', 'error');return;}// 验证重试次数if (isNaN(maxRetryCount) || maxRetryCount < 1 || maxRetryCount > 10) {addLog('重试次数必须是1-10之间的数字', 'error');return;}// 验证TXTAPI链接(仅在API模式下)if (useTxtApi && txtApiMode === 'api' && !txtApiUrl) {addLog('请填写TXTAPI链接', 'error');return;}// 验证手动文本(仅在手动模式下)if (useTxtApi && txtApiMode === 'manual' && !txtApiManualText.trim()) {addLog('请填写手动文本内容', 'error');return;}// 更新配置userConfig.sendTime = timeValue;userConfig.targetUsername = targetUsername;userConfig.useHitokoto = useHitokoto;userConfig.useTxtApi = useTxtApi;userConfig.txtApiMode = txtApiMode;userConfig.txtApiManualRandom = txtApiRandom;userConfig.txtApiUrl = txtApiUrl;userConfig.txtApiManualText = txtApiManualText;userConfig.maxRetryCount = maxRetryCount;userConfig.hitokotoFormat = hitokotoFormat;userConfig.fromFormat = fromFormat;userConfig.fromWhoFormat = fromWhoFormat;userConfig.customMessage = customMessage;// 保存配置saveConfig();// 更新状态updateStatus(GM_getValue('lastSentDate', '') === new Date().toDateString());updateRetryCount();// 关闭设置面板settingsPanel.remove();addLog('设置已保存', 'success');// 重启聊天观察器initDynamicChatClick();});}// 显示历史日志面板function showHistoryPanel() {// 移除可能已存在的历史面板const existingHistory = document.getElementById('dy-fire-history-panel');if (existingHistory) {existingHistory.remove();return;}const historyPanel = document.createElement('div');historyPanel.id = 'dy-fire-history-panel';historyPanel.style.cssText = `position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);max-width: 90vw;width: 600px;background: white;border-radius: 12px;box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);z-index: 10000;padding: 20px;font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;max-height: 90vh;overflow: hidden;display: flex;flex-direction: column;box-sizing: border-box;`;// 获取所有日志const logs = GM_getValue('allLogs', []);historyPanel.innerHTML = `<h3 style="margin: 0 0 20px 0; color: #ff2c54; display: flex; justify-content: space-between; align-items: center;"><span>历史日志 (${logs.length} 条)</span><button id="dy-fire-history-close" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #999;">×</button></h3><div style="display: flex; justify-content: space-between; margin-bottom: 10px;"><button id="dy-fire-history-clear" style="padding: 6px 12px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">清空所有日志</button><button id="dy-fire-history-export" style="padding: 6px 12px; background: #17a2b8; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">导出日志</button></div><div id="dy-fire-history-content" style="flex: 1; overflow-y: auto; border: 1px solid #eee; border-radius: 4px; padding: 10px; font-size: 12px; line-height: 1.4;">${logs.length === 0 ? '<div style="text-align: center; color: #999; padding: 20px;">暂无历史日志</div>' : ''}${logs.map(log => `<div style="margin-bottom: 5px; color: ${log.type === 'success' ? '#28a745' : log.type === 'error' ? '#dc3545' : log.type === 'warning' ? '#ffc107' : '#17a2b8'};">${log.time} - ${log.message}</div>`).join('')}</div>`;document.body.appendChild(historyPanel);// 添加关闭按钮事件document.getElementById('dy-fire-history-close').addEventListener('click', function() {historyPanel.remove();});// 添加清空日志按钮事件document.getElementById('dy-fire-history-clear').addEventListener('click', function() {if (confirm('确定要清空所有历史日志吗?此操作不可恢复。')) {GM_setValue('allLogs', []);allLogs = [];document.getElementById('dy-fire-history-content').innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">暂无历史日志</div>';addLog('历史日志已清空', 'info');}});// 添加导出日志按钮事件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');});// 自动滚动到底部const content = document.getElementById('dy-fire-history-content');if (content) {content.scrollTop = content.scrollHeight;}}// 添加日志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);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;}}// 更新重试计数显示function updateRetryCount() {const retryElement = document.getElementById('dy-fire-retry');if (retryElement) {retryElement.textContent = `${retryCount}/${userConfig.maxRetryCount}`;}}// 更新一言状态显示function updateHitokotoStatus(status, isSuccess = true) {const statusEl = document.getElementById('dy-fire-hitokoto');if (statusEl) {statusEl.textContent = status;statusEl.style.color = isSuccess ? '#28a745' : '#dc3545';}}// 更新TXTAPI状态显示function updateTxtApiStatus(status, isSuccess = true) {const statusEl = document.getElementById('dy-fire-txtapi');if (statusEl) {statusEl.textContent = status;statusEl.style.color = isSuccess ? '#28a745' : '#dc3545';}}// 更新聊天目标状态显示function updateChatTargetStatus(status, isSuccess = true) {const statusEl = document.getElementById('dy-fire-chat-target');if (statusEl) {statusEl.textContent = status;statusEl.style.color = isSuccess ? '#28a745' : '#dc3545';}}// 获取消息内容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;}// 获取一言内容function getHitokoto() {return new Promise((resolve, reject) => {const timeout = setTimeout(() => {reject(new Error('一言API请求超时'));}, userConfig.hitokotoTimeout);GM_xmlhttpRequest({method: 'GET',url: 'https://hit.cn/',responseType: 'json',onload: function(response) {clearTimeout(timeout);if (response.status === 200) {try {const data = response.response;let message = formatHitokoto(userConfig.hitokotoFormat, data);updateHitokotoStatus('获取成功');resolve(message);} catch (e) {updateHitokotoStatus('解析失败', false);reject(new Error('一言API响应解析失败'));}} else {updateHitokotoStatus('请求失败', false);reject(new Error(`一言API请求失败: ${response.status}`));}},onerror: function(error) {clearTimeout(timeout);updateHitokotoStatus('网络错误', false);reject(new Error('一言API网络错误'));},ontimeout: function() {clearTimeout(timeout);updateHitokotoStatus('请求超时', false);reject(new Error('一言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;}// 获取TXTAPI内容function getTxtApiContent() {return new Promise((resolve, reject) => {if (userConfig.txtApiMode === 'api') {// API模式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) {try {updateTxtApiStatus('获取成功');resolve(response.responseText.trim());} catch (e) {updateTxtApiStatus('解析失败', false);reject(new Error('TXTAPI响应解析失败'));}} else {updateTxtApiStatus('请求失败', false);reject(new Error(`TXTAPI请求失败: ${response.status}`));}},onerror: function(error) {clearTimeout(timeout);updateTxtApiStatus('网络错误', false);reject(new Error('TXTAPI网络错误'));},ontimeout: function() {clearTimeout(timeout);updateTxtApiStatus('请求超时', false);reject(new Error('TXTAPI请求超时'));}});} else {// 手动模式try {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);}} catch (e) {updateTxtApiStatus('解析失败', false);reject(new Error('手动文本解析失败'));}}});}// 发送消息函数async function sendMessage() {if (isProcessing) {addLog('已有任务正在进行中', 'error');return;}// 检查是否今天已发送const lastSentDate = GM_getValue('lastSentDate', '');const today = new Date().toDateString();if (lastSentDate === today) {addLog('今天已经发送过消息', 'info');return;}isProcessing = true;retryCount = 0;updateRetryCount();addLog('开始发送流程...', 'info');// 执行发送流程executeSendProcess();}// 执行发送流程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);}}// 查找并点击目标用户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); // 10秒超时}// 尝试点击目标用户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();addLog(`成功点击动态加载的用户: ${userConfig.targetUsername}`, 'success');hasClickedTarget = true;updateChatTargetStatus('已找到', true);// 等待聊天界面加载,然后查找聊天输入框setTimeout(() => {addLog('目标用户已点击,等待聊天界面加载...', 'info');setTimeout(tryFindChatInput, 2000);}, 1000);return true;}}}return false;} catch (error) {addLog(`寻找目标用户时出错: ${error.message}`, 'error');return false;}}// 尝试查找聊天输入框并发送消息async function tryFindChatInput() {const inputSelectors = ['.chat-input-dccKiL','[class*="input"]','[class*="textarea"]','[contenteditable="true"]','textarea','input[type="text"]'];let input = null;for (let selector of inputSelectors) {input = document.querySelector(selector);if (input) break;}if (input) {addLog('找到聊天输入框', 'info');// 获取消息内容let messageToSend;try {messageToSend = await getMessageContent();addLog('消息内容准备完成', 'success');} catch (error) {addLog(`消息获取失败: ${error.message}`, 'error');messageToSend = `${userConfig.baseMessage} | 消息获取失败~`;}// 清空输入框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 }));// 查找发送按钮const sendBtnSelectors = ['.chat-btn','[class*="send"]','[class*="button"]:not([disabled])','button:not([disabled])'];let sendBtn = null;for (let selector of sendBtnSelectors) {sendBtn = document.querySelector(selector);if (sendBtn) break;}// 检查发送按钮状态setTimeout(() => {if (sendBtn && !sendBtn.disabled) {addLog('正在发送消息...', 'info');sendBtn.click();// 确认发送成功setTimeout(() => {addLog('消息发送成功!', 'success');// 立即记录发送状态const today = new Date().toDateString();GM_setValue('lastSentDate', today);// 更新状态显示updateStatus(true);isProcessing = false;// 显示通知if (typeof GM_notification !== 'undefined') {try {GM_notification({title: '抖音续火助手',text: '续火消息发送成功!',timeout: 3000});} catch (e) {GM_notification('续火消息发送成功!', '抖音续火助手');}}}, 1000);} else {addLog('发送按钮不可用或未找到', 'error');setTimeout(executeSendProcess, 2000);}}, 500);} else {addLog('未找到输入框,重试中...', 'error');setTimeout(executeSendProcess, 2000);}}// 解析时间字符串为日期对象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 updateStatus(isSent) {const statusEl = document.getElementById('dy-fire-status');if (statusEl) {if (isSent) {statusEl.textContent = '已发送';statusEl.style.color = '#28a745';} else {statusEl.textContent = '未发送';statusEl.style.color = '#dc3545';// 如果是"未发送"状态,检查是否需要自动发送autoSendIfNeeded();}}// 更新下次发送时间const now = new Date();if (isSent) {// 如果已发送,下次发送时间是明天指定时间nextSendTime = parseTimeString(userConfig.sendTime);// 确保是明天而不是后天const tomorrow = new Date(now);tomorrow.setDate(tomorrow.getDate() + 1);if (nextSendTime.getDate() !== tomorrow.getDate()) {nextSendTime.setDate(tomorrow.getDate());}} else {// 如果未发送,检查当前时间是否已过指定时间nextSendTime = parseTimeString(userConfig.sendTime);// 如果目标时间已经过去,设置为明天if (nextSendTime <= now) {nextSendTime.setDate(nextSendTime.getDate() + 1);}}const nextElement = document.getElementById('dy-fire-next');if (nextElement) {nextElement.textContent = nextSendTime.toLocaleString();}// 开始倒计时startCountdown(nextSendTime);}// 检查是否需要自动发送function autoSendIfNeeded() {const now = new Date();const today = new Date().toDateString();const lastSentDate = GM_getValue('lastSentDate', '');// 解析发送时间const [targetHour, targetMinute, targetSecond] = userConfig.sendTime.split(':').map(Number);// 如果今天未发送且当前时间已过指定时间,则自动发送if (lastSentDate !== today) {const targetTimeToday = new Date();targetTimeToday.setHours(targetHour, targetMinute, targetSecond || 0, 0);if (now >= targetTimeToday && !isProcessing) {addLog(`检测到今日未发送且已过${userConfig.sendTime},自动发送`, 'info');sendMessage();}}}// 开始倒计时function startCountdown(targetTime) {// 清除之前的倒计时if (countdownInterval) {clearInterval(countdownInterval);}function update() {const now = new Date();const diff = targetTime - now;if (diff <= 0) {const countdownElement = document.getElementById('dy-fire-countdown');if (countdownElement) {countdownElement.textContent = '00:00:00';}// 检查是否今天已发送const lastSentDate = GM_getValue('lastSentDate', '');const today = new Date().toDateString();if (lastSentDate === today) {// 如果已发送,设置明天指定时间为目标时间nextSendTime = parseTimeString(userConfig.sendTime);// 确保是明天而不是后天const tomorrow = new Date(now);tomorrow.setDate(tomorrow.getDate() + 1);if (nextSendTime.getDate() !== tomorrow.getDate()) {nextSendTime.setDate(tomorrow.getDate());}startCountdown(nextSendTime);} else {// 如果未发送,清空数据并发送消息if (!isProcessing) {// 清空数据,避免重复发送GM_setValue('lastSentDate', '');updateStatus(false);addLog('已清空发送记录,准备发送新消息', 'info');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);const countdownElement = document.getElementById('dy-fire-countdown');if (countdownElement) {countdownElement.textContent =`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;}}update();countdownInterval = setInterval(update, 1000);}// 动态聊天列表点击功能function initDynamicChatClick() {// 停止之前的观察器和定时器stopDynamicChatClick();// 刷新页面后重置为寻找中hasClickedTarget = false;updateChatTargetStatus('寻找中', false);function clickIfFound() {if (hasClickedTarget) return;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();addLog(`成功点击动态加载的用户: ${userConfig.targetUsername}`, 'success');hasClickedTarget = true;updateChatTargetStatus('已找到', true);// 找到后停止观察器stopDynamicChatClick();return true;}}}return false;} catch (error) {addLog(`寻找目标用户时出错: ${error.message}`, 'error');return false;}}// 使用MutationObserver监听DOM变化,但限制观察范围chatObserver = new MutationObserver((mutations) => {// 修复:检查mutations是否为可迭代对象if (!mutations || typeof mutations[Symbol.iterator] !== 'function') {return;}try {for (let mutation of mutations) {if (mutation.addedNodes.length > 0 && !hasClickedTarget) {clickIfFound();break;}}} catch (error) {addLog(`处理DOM变化时出错: ${error.message}`, 'error');}});// 只观察body的子节点变化,减少性能消耗try {chatObserver.observe(document.body, {childList: true,subtree: true // 改为true,观察所有子节点});} catch (error) {addLog(`启动DOM观察器时出错: ${error.message}`, 'error');}// 初始尝试,但限制尝试次数let initialAttempts = 0;const maxInitialAttempts = 5;function initialTry() {if (hasClickedTarget || initialAttempts >= maxInitialAttempts) return;if (clickIfFound()) {return;}initialAttempts++;if (initialAttempts < maxInitialAttempts) {initialAttemptTimer = setTimeout(initialTry, 1000);} else {updateChatTargetStatus('未找到', false);addLog(`未找到目标用户: ${userConfig.targetUsername}`, 'warning');}}// 延迟初始尝试,等待页面加载initialAttemptTimer = setTimeout(initialTry, 2000);}// 停止动态聊天点击功能function stopDynamicChatClick() {if (chatObserver) {chatObserver.disconnect();chatObserver = null;}if (sendProcessObserver) {sendProcessObserver.disconnect();sendProcessObserver = null;}if (initialAttemptTimer) {clearTimeout(initialAttemptTimer);initialAttemptTimer = null;}if (debounceTimer) {clearTimeout(debounceTimer);debounceTimer = null;}}// 清空数据function clearData() {GM_setValue('lastSentDate', '');GM_setValue('txtApiManualSentIndexes', []);addLog('发送记录已清空', 'info');updateStatus(false);retryCount = 0;updateRetryCount();updateHitokotoStatus('未获取');updateTxtApiStatus('未获取');hasClickedTarget = false; // 重置目标用户状态updateChatTargetStatus('寻找中', false);// 重启聊天观察器initDynamicChatClick();}// 重置所有配置function resetAllConfig() {// 获取所有存储的值并删除if (typeof GM_listValues !== 'undefined' && typeof GM_deleteValue !== 'undefined') {try {const values = GM_listValues();values.forEach(key => {GM_deleteValue(key);});} catch (e) {GM_setValue('lastSentDate', '');GM_setValue('userConfig', '');GM_setValue('txtApiManualSentIndexes', []);GM_setValue('allLogs', []);}} else {// 如果GM_listValues不可用,只删除已知的keyGM_setValue('lastSentDate', '');GM_setValue('userConfig', '');GM_setValue('txtApiManualSentIndexes', []);GM_setValue('allLogs', []);}// 重新初始化配置initConfig();addLog('所有配置已重置', 'info');updateStatus(false);retryCount = 0;updateRetryCount();updateHitokotoStatus('未获取');updateTxtApiStatus('未获取');hasClickedTarget = false; // 重置目标用户状态updateChatTargetStatus('寻找中', false);// 显示通知if (typeof GM_notification !== 'undefined') {try {GM_notification({title: '抖音续火助手',text: '所有配置已重置!',timeout: 3000});} catch (e) {GM_notification('所有配置已重置!', '抖音续火助手');}}// 重启聊天观察器initDynamicChatClick();}// 初始化函数function init() {// 初始化配置initConfig();// 创建控制面板createControlPanel();// 初始化动态聊天列表点击功能initDynamicChatClick();// 检查今天是否已发送const lastSentDate = GM_getValue('lastSentDate', '');const today = new Date().toDateString();const isSentToday = lastSentDate === today;// 更新状态updateStatus(isSentToday);// 注册菜单命令if (typeof GM_registerMenuCommand !== 'undefined') {try {GM_registerMenuCommand('抖音续火助手-显示面板', function() {const panel = document.getElementById('dy-fire-helper');if (panel) {panel.style.display = 'block';}});GM_registerMenuCommand('立即发送续火消息', sendMessage);GM_registerMenuCommand('设置', showSettingsPanel);GM_registerMenuCommand('历史日志', showHistoryPanel);GM_registerMenuCommand('清空发送记录', clearData);GM_registerMenuCommand('重置所有配置', resetAllConfig);} catch (e) {addLog('菜单命令注册失败,使用面板控制', 'error');}}addLog('抖音续火助手已启动', 'info');// 优化定时检查:改为每5分钟检查一次,而不是每分钟setInterval(() => {const now = new Date();const [targetHour, targetMinute] = userConfig.sendTime.split(':').map(Number);// 只在目标时间前后5分钟内进行检查if (Math.abs(now.getHours() - targetHour) <= 1 &&Math.abs(now.getMinutes() - targetMinute) <= 5) {checkAndSend();}}, 300000); // 5分钟检查一次function checkAndSend() {const now = new Date();const [targetHour, targetMinute] = userConfig.sendTime.split(':').map(Number);if (now.getHours() === targetHour && now.getMinutes() === targetMinute) {const lastSentDate = GM_getValue('lastSentDate', '');const today = new Date().toDateString();if (lastSentDate !== today && !isProcessing) {addLog('定时检查:准备发送消息', 'info');sendMessage();}}}}// 等待页面加载完成后初始化if (document.readyState === 'loading') {document.addEventListener('DOMContentLoaded', init);} else {init();}})();
解析
该脚本为某音续火花自动发送脚本。
主要作用
定时自动续火
每天在指定时间(默认 00:01:00)自动给目标用户发送续火消息,支持重试、倒计时与状态展示。内容自动拼装
调用「一言」API(v1.hitokoto.cn)并按自定义格式插入。
TXTAPI 两种模式:远程 API 拉取文本,或本地手动文本库(顺序/随机且带"已发送索引"轮转机制)。
将上述内容替换到自定义模板占位符
[API]、[TXTAPI]中生成最终消息。自动定位聊天对象并发送
通过多套选择器+MutationObserver 动态查找聊天列表中的目标用户名并点击进入会话;找到输入框后自动填入多行文本并点击发送按钮。可视化控制面板 + 历史日志
浮动面板展示今日状态、下次发送时间、倒计时、重试次数、一言/TXTAPI状态、目标用户状态;提供"立即发送/设置/历史日志/清空记录/重置配置"等操作,并持久化运行日志(可导出)。
主要方法
配置与面板
initConfig()/saveConfig()
合并默认与已保存配置,确保字段完整;保存到GM_setValue。createControlPanel()
搭建右上角悬浮面板(状态、按钮、日志区域),绑定按钮事件:立即发送、设置、历史日志、清空、重置。showSettingsPanel()
设置项编辑与校验(发送时间、目标用户名、一言/TXTAPI开关与模式、格式模板、重试次数、自定义消息等),保存后重载聊天监听。showHistoryPanel()
读取allLogs展示历史记录,支持清空与导出.txt。
日志与状态
addLog(message, type)
写入内存+持久化日志(限量滚动),同步到面板日志区(着色)。updateRetryCount()/updateHitokotoStatus()/updateTxtApiStatus()/updateChatTargetStatus()
更新面板上的重试次数与各模块状态文案/颜色。parseTimeString(timeStr)
将HH:mm:ss转为下次执行的Date(若已过则推到明日)。updateStatus(isSent)
设置"已/未发送"、计算并展示下次发送时间,并启动倒计时。startCountdown(targetTime)
1s 刷新倒计时;到点后触发自动发送或滚动到明天。autoSendIfNeeded()
若"今日未发"且已过指定时间,自动调用发送流程。
内容生成
getMessageContent()
串行获取一言与 TXTAPI 内容,替换到自定义模板的[API]、[TXTAPI],返回最终要发的文本。getHitokoto()→formatHitokoto(format, data)
请求一言 JSON,并按hitokotoFormat,fromFormat,fromWhoFormat组装显示(当 from / from_who 为空时对应格式不显示)。getTxtApiContent()API 模式:GET 指定 URL,返回文本。
手动模式:从多行文本中顺序或随机取一条;带"已发索引"记忆与"轮转重置"。
发送流程(核心链路)
sendMessage()
入口:检查是否"今日已发/正在进行",初始化重试计数后进入执行流程。executeSendProcess()
控制重试节奏;若未找到目标用户先定位用户,已找到则去找输入框。findAndClickTargetUser()/tryClickTargetUser()
多套选择器扫描聊天列表文本;匹配到targetUsername就点击并标记hasClickedTarget,随后等待界面加载去找输入框。若超时则重试。tryFindChatInput()
多选择器寻找输入框与发送按钮;将拼装好的多行消息逐行插入(含换行)、触发input事件,找到"可用"的发送按钮后点击;成功则记录lastSentDate、更新状态与通知;否则重试。
动态聊天列表监听
initDynamicChatClick()
启动MutationObserver监听 DOM 变化 + 若干次延迟"初始尝试",在列表异步渲染时自动点击目标用户;找到后停止观察器。stopDynamicChatClick()
关闭相关 Observer/定时器(避免资源占用)。
数据维护
clearData()
清空"今日发送记录""手动文本发送索引"等状态,并重启聊天监听。resetAllConfig()
尝试清空所有 GM 存储键,恢复默认配置与状态,重新启动监听。
初始化
init()
检测环境 → 初始化配置/面板 → 启动聊天监听 → 根据lastSentDate更新状态/倒计时 → 注册右键菜单命令 → 每 5 分钟做一次"接近目标时间"的轻量检查以触发定时发送。
注意:
本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。
没有评论:
发表评论