// ==UserScript== // @name 抖音私信自动发送 // @namespace http://tampermonkey.net/ // @version 0.3 // @description 自动给指定好友发送一言文心每日一句 // @author @hao // @match https://www.douyin.com/chat* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; const CONFIG_KEY = 'douyin_chat_friends'; const SEND_TYPE_KEY = 'douyin_chat_send_type'; const SCHEDULE_TIME_KEY = 'douyin_chat_schedule_time'; const MONITOR_SERVER_KEY = 'douyin_chat_monitor_server'; const PUSHPLUS_TOKEN = ''; const REFRESH_INTERVAL = 10 * 60 * 1000; const MONITOR_REPORT_INTERVAL = 10 * 1000; // 发送状态持久化 Key const LAST_SENT_DATE_KEY = 'douyin_chat_last_sent_date'; const SCHEDULE_STATUS_KEY = 'douyin_chat_schedule_status'; const LOG_KEY = 'douyin_chat_logs'; const CONFIG = { targetFriends: GM_getValue(CONFIG_KEY, []), sendType: GM_getValue(SEND_TYPE_KEY, 'text'), scheduleTime: GM_getValue(SCHEDULE_TIME_KEY, ''), monitorServer: GM_getValue(MONITOR_SERVER_KEY, 'http://localhost:5173'), messageApi: 'https://v1.hitokoto.cn/', sendDelay: 2000, intervalDelay: 4000, searchInputSelector: '.semi-input.semi-input-default', conversationItemSelector: '[data-e2e="conversation-item"]', conversationTitleSelector: '.conversationConversationItemtitle', conversationClickAreaSelector: '.conversationConversationItemrowArea2', chatEditorSelector: '.messageEditorimChatEditorContainer', chatInputSelector: '[contenteditable="true"][data-slate-editor]', sendBtnSelector: '.messageMsgInputpublishBtn', emojiBtnSelector: '.messageMsgInputiconAction', emojiPanelSelector: '.componentsemojiim-saas-modal', emojiImgSelector: '.commonMyImageimgReal' }; // 获取北京时间日期字符串 YYYY-MM-DD function getTodayDateStr() { const now = new Date(); const utc = now.getTime() + now.getTimezoneOffset() * 60000; const bj = new Date(utc + 8 * 3600000); const y = bj.getFullYear(); const m = String(bj.getMonth() + 1).padStart(2, '0'); const d = String(bj.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } // 记录当天已发送的日期(同时写入 GM_setValue 和 localStorage) function setLastSentDate(dateStr) { GM_setValue(LAST_SENT_DATE_KEY, dateStr); try { localStorage.setItem(LAST_SENT_DATE_KEY, dateStr); } catch (e) {} } // 获取上次发送日期(优先读 GM_setValue,回退 localStorage) function getLastSentDate() { let val = GM_getValue(LAST_SENT_DATE_KEY, ''); if (!val) { try { val = localStorage.getItem(LAST_SENT_DATE_KEY) || ''; } catch (e) {} } return val; } // 判断今天是否已发送 function isTodaySent() { return getLastSentDate() === getTodayDateStr(); } async function fetchMessage() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: CONFIG.messageApi + '?c=a&c=b&c=c&c=d&c=e&c=f&c=h&c=i&c=k&c=l', onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.hitokoto) { const text = data.hitokoto; let source = ''; if (data.from_who) { source = data.from_who; } else if (data.from) { source = data.from; } if (source) { resolve(text + ' ——' + source); } else { resolve(text); } } else { resolve('今日无事,聊以自娱。'); } } catch (e) { resolve('今日无事,聊以自娱。'); } }, onerror: function() { resolve('今日无事,聊以自娱。'); } }); }); } function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { const element = document.querySelector(selector); if (element) { resolve(element); } else if (Date.now() - startTime < timeout) { setTimeout(check, 300); } else { reject(new Error('Element not found: ' + selector)); } }; check(); }); } function waitForCondition(condition, timeout = 10000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { if (condition()) { resolve(); } else if (Date.now() - startTime < timeout) { setTimeout(check, 300); } else { reject(new Error('Condition not met')); } }; check(); }); } async function searchFriend(name) { try { const searchInput = await waitForElement(CONFIG.searchInputSelector, 5000); searchInput.value = ''; searchInput.dispatchEvent(new Event('input', { bubbles: true })); await new Promise(resolve => setTimeout(resolve, 300)); searchInput.value = name; searchInput.dispatchEvent(new Event('input', { bubbles: true })); await new Promise(resolve => setTimeout(resolve, 1500)); return true; } catch (e) { console.log('搜索失败:', e.message); return false; } } async function findFriendByName(name) { const items = document.querySelectorAll(CONFIG.conversationItemSelector); for (const item of items) { const title = item.querySelector(CONFIG.conversationTitleSelector); if (title) { const text = title.textContent ? title.textContent.trim() : ''; if (text && (text.includes(name) || name.includes(text))) { return item; } } } return null; } async function clickFriend(name) { let friendItem = await findFriendByName(name); if (!friendItem) { const searched = await searchFriend(name); if (!searched) { console.log(`搜索好友失败: ${name}`); return false; } await new Promise(resolve => setTimeout(resolve, 1000)); friendItem = await findFriendByName(name); } if (!friendItem) { console.log(`未找到好友: ${name}`); return false; } const clickArea = friendItem.querySelector(CONFIG.conversationClickAreaSelector); const targetElement = clickArea || friendItem; const rect = targetElement.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; targetElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y })); await new Promise(resolve => setTimeout(resolve, 50)); targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x, clientY: y })); await new Promise(resolve => setTimeout(resolve, 50)); targetElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y })); await new Promise(resolve => setTimeout(resolve, 2000)); return true; } async function getChatInput() { try { return await waitForElement(CONFIG.chatInputSelector, 10000); } catch (e) { console.log('未找到输入框:', e.message); return null; } } async function getSendButton() { try { return await waitForElement(CONFIG.sendBtnSelector, 5000); } catch (e) { console.log('未找到发送按钮:', e.message); return null; } } async function sendMessage(message) { const input = await getChatInput(); if (!input) { console.log('未找到输入框'); return false; } input.focus(); await new Promise(resolve => setTimeout(resolve, 200)); document.execCommand('selectAll', false); document.execCommand('delete', false); document.execCommand('insertText', false, message); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); await new Promise(resolve => setTimeout(resolve, 800)); const sendBtn = await getSendButton(); if (!sendBtn) { console.log('未找到发送按钮,尝试按Enter发送'); try { input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); await new Promise(resolve => setTimeout(resolve, 1000)); return true; } catch (e) { console.log('Enter发送失败:', e.message); return false; } } const rect = sendBtn.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; sendBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y })); await new Promise(resolve => setTimeout(resolve, 1000)); return true; } async function sendEmoji() { try { const emojiBtn = await waitForElement(CONFIG.emojiBtnSelector, 5000); console.log('找到表情按钮:', emojiBtn); emojiBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); await new Promise(resolve => setTimeout(resolve, 1500)); let emojiItem = document.querySelector('.emojiEmojiItemimgBox'); if (!emojiItem) { const emojiPanel = document.querySelector('.componentsemojiim-saas-modal'); if (emojiPanel) { emojiItem = emojiPanel.querySelector('.emojiEmojiItemimgBox'); } } if (!emojiItem) { emojiItem = document.querySelector('.emojiEmojiItememojiItem'); if (!emojiItem && emojiPanel) { emojiItem = emojiPanel.querySelector('.emojiEmojiItememojiItem'); } } if (!emojiItem) { console.log('未找到表情项'); return false; } console.log('找到表情项:', emojiItem); const rect = emojiItem.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; emojiItem.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y })); await new Promise(resolve => setTimeout(resolve, 50)); emojiItem.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x, clientY: y })); await new Promise(resolve => setTimeout(resolve, 50)); emojiItem.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y })); await new Promise(resolve => setTimeout(resolve, 1500)); return true; } catch (e) { console.log('发送表情失败:', e.message); return false; } } function sendPushPlus(title, content) { GM_xmlhttpRequest({ method: 'POST', url: 'https://www.pushplus.plus/send', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ token: PUSHPLUS_TOKEN, title: title, content: content, template: 'html' }), onload: function(response) { console.log('PushPlus推送结果:', response.responseText); }, onerror: function() { console.log('PushPlus推送失败'); } }); } let scheduleTimer = null; let refreshTimer = null; let qrWatchTimer = null; let lastQrCode = ''; let qrRefreshCount = 0; let lastTriggeredMinute = -1; const MAX_QR_REFRESH = 10; function getBeijingTime() { const now = new Date(); const utc = now.getTime() + now.getTimezoneOffset() * 60000; return new Date(utc + 8 * 3600000); } function startSchedule(scheduleTime) { if (scheduleTimer) { clearInterval(scheduleTimer); } if (!scheduleTime) { console.log('未设置定时时间'); return; } const [hours, minutes] = scheduleTime.split(':').map(Number); if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { console.log('定时时间格式错误'); return; } console.log(`定时发送已设置: 每天 ${scheduleTime}`); scheduleTimer = setInterval(async () => { const now = getBeijingTime(); const currentHours = now.getHours(); const currentMinutes = now.getMinutes(); const currentSeconds = now.getSeconds(); // 跨天重置:检查日期是否变化 const todayStr = getTodayDateStr(); const lastDate = getLastSentDate(); if (lastDate && lastDate !== todayStr) { // 跨天,清除昨日发送记录 setLastSentDate(''); lastTriggeredMinute = -1; } // 当前时间戳(用于去重,防止同一分钟内多次触发) const currentMinuteKey = currentHours * 60 + currentMinutes; // 检查是否到达设定时间(允许前后3秒误差) if (currentHours === hours && currentMinutes === minutes && currentSeconds >= 0 && currentSeconds <= 5 && lastTriggeredMinute !== currentMinuteKey) { // 检查今天是否已发送 if (isTodaySent()) { console.log('今天已发送,跳过'); lastTriggeredMinute = currentMinuteKey; return; } console.log('定时发送触发'); appendLog('schedule_trigger', '-', ''); lastTriggeredMinute = currentMinuteKey; if (refreshTimer) { clearInterval(refreshTimer); } await executeSend(); sendPushPlus('定时发送完成', `北京时间 ${scheduleTime},已成功发送消息`); startRefreshTimer(); } }, 1000); // 定时状态写入 localStorage try { localStorage.setItem(SCHEDULE_STATUS_KEY, JSON.stringify({ status: 'active', time: scheduleTime })); } catch (e) {} } function stopSchedule() { if (scheduleTimer) { clearInterval(scheduleTimer); scheduleTimer = null; console.log('定时发送已停止'); } // 定时状态写入 localStorage try { localStorage.setItem(SCHEDULE_STATUS_KEY, JSON.stringify({ status: 'inactive' })); } catch (e) {} } function startRefreshTimer() { if (refreshTimer) { clearInterval(refreshTimer); } refreshTimer = setInterval(() => { checkLoginStatus(); }, REFRESH_INTERVAL); console.log('10分钟自动刷新已启动'); } function stopRefreshTimer() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; console.log('10分钟自动刷新已停止'); } } function startQrWatch() { if (qrWatchTimer) { clearInterval(qrWatchTimer); } qrRefreshCount = 0; qrWatchTimer = setInterval(() => { const allImages = Array.from(document.querySelectorAll('img')); const qrCode = allImages.find(img => img.src.startsWith('data:image/png;base64') && img.width > 100 && img.height > 100) || document.querySelector('img[src*="qr"], img[src*="QR"]'); if (qrCode && qrCode.src !== lastQrCode) { console.log('检测到二维码更新'); qrRefreshCount++; lastQrCode = qrCode.src; let base64 = qrCode.src; if (!base64.startsWith('data:image')) { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); base64 = canvas.toDataURL('image/png'); if (qrRefreshCount >= MAX_QR_REFRESH) { sendPushPlus('⚠️ 登录凭证即将过期', `已推送 ${qrRefreshCount} 次二维码,您仍未扫码登录。请尽快登录,否则定时发送将暂停。`); stopQrWatch(); } else { sendPushPlus('登录二维码已更新', `二维码`); } }; img.src = qrCode.src; } catch (e) { console.log('二维码更新推送失败:', e.message); } } else { if (qrRefreshCount >= MAX_QR_REFRESH) { sendPushPlus('⚠️ 登录凭证即将过期', `已推送 ${qrRefreshCount} 次二维码,您仍未扫码登录。请尽快登录,否则定时发送将暂停。`); stopQrWatch(); } else { sendPushPlus('登录二维码已更新', `二维码`); } } } }, 5000); console.log('二维码监控已启动'); } function stopQrWatch() { if (qrWatchTimer) { clearInterval(qrWatchTimer); qrWatchTimer = null; lastQrCode = ''; qrRefreshCount = 0; console.log('二维码监控已停止'); } } async function checkLoginStatus() { const now = getBeijingTime(); const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; const loginModal = document.querySelector('[class*="login"], [class*="Login"], [class*="qr"], [class*="QR"]'); const allImages = Array.from(document.querySelectorAll('img')); const qrCode = allImages.find(img => img.src.startsWith('data:image/png;base64') && img.width > 100 && img.height > 100) || document.querySelector('img[src*="qr"], img[src*="QR"]'); if (loginModal || qrCode) { console.log('检测到需要登录'); appendLog('error', '-', 'login_required'); if (!qrWatchTimer) { sendPushPlus('账号需要登录', `北京时间 ${timeStr},抖音账号已掉线,请扫码登录`); startQrWatch(); } checkSecurityVerification(); } else { console.log('账号登录状态正常,执行页面刷新'); stopQrWatch(); location.reload(); } } async function checkSecurityVerification() { await new Promise(resolve => setTimeout(resolve, 3000)); const pwdInput = document.querySelector('input[type="password"]'); const verifyBtn = document.querySelector('[class*="verify"], [class*="confirm"], button'); if (pwdInput) { console.log('检测到安全认证,自动填写密码'); pwdInput.value = 'xuancai520'; pwdInput.dispatchEvent(new Event('input', { bubbles: true })); if (verifyBtn) { setTimeout(() => { verifyBtn.click(); }, 500); } } } // 运行日志记录 function appendLog(type, friendName, message) { const logEntry = { time: new Date().toISOString(), type, friend: friendName, message }; let logs = []; try { logs = JSON.parse(localStorage.getItem(LOG_KEY) || '[]'); } catch (e) { logs = []; } logs.push(logEntry); // 超过 100 条,移除最旧的 while (logs.length > 100) { logs.shift(); } try { localStorage.setItem(LOG_KEY, JSON.stringify(logs)); } catch (e) {} } let reportTimer = null; function reportToMonitorServer() { const serverUrl = CONFIG.monitorServer; if (!serverUrl) return; let scheduleStatus = null; try { scheduleStatus = JSON.parse(localStorage.getItem(SCHEDULE_STATUS_KEY) || 'null'); } catch (e) {} if (!scheduleStatus) { const t = GM_getValue(SCHEDULE_TIME_KEY, ''); if (t) scheduleStatus = { status: 'active', time: t }; } let logs = []; try { logs = JSON.parse(localStorage.getItem(LOG_KEY) || '[]'); } catch (e) {} const data = { scheduleStatus: scheduleStatus, scheduleTime: CONFIG.scheduleTime, lastSentDate: getLastSentDate(), friends: CONFIG.targetFriends, logs: logs }; try { GM_xmlhttpRequest({ method: 'POST', url: serverUrl.replace(/\/$/, '') + '/api/data', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data), onerror: function() {} }); } catch (e) {} } function startMonitorReport() { if (reportTimer) clearInterval(reportTimer); reportTimer = setInterval(reportToMonitorServer, MONITOR_REPORT_INTERVAL); reportToMonitorServer(); } async function executeSend() { const friends = CONFIG.targetFriends; const sendType = CONFIG.sendType; let customMsg = ''; if (friends.length === 0) { console.log('好友列表为空'); return; } for (let i = 0; i < friends.length; i++) { console.log(`正在发送给: ${friends[i]}...`); const success = await processFriend(friends[i], customMsg, sendType); if (success) { console.log(`成功发送给: ${friends[i]}`); } else { console.log(`发送失败: ${friends[i]}`); } if (i < friends.length - 1) { await new Promise(resolve => setTimeout(resolve, CONFIG.intervalDelay)); } } setLastSentDate(getTodayDateStr()); console.log('=== 定时发送完成 ==='); } async function processFriend(friendName, customMsg = '', sendType = 'text') { console.log(`正在处理好友: ${friendName}`); const found = await clickFriend(friendName); if (!found) { return false; } await new Promise(resolve => setTimeout(resolve, 2000)); let success; if (sendType === 'emoji') { success = await sendEmoji(); if (success) { console.log(`成功给 ${friendName} 发送表情`); appendLog('send_success', friendName, ''); } else { console.log(`给 ${friendName} 发送表情失败`); appendLog('send_fail', friendName, ''); } } else { let message; if (customMsg.trim()) { message = customMsg; } else { message = await fetchMessage(); } success = await sendMessage(message); if (success) { console.log(`成功给 ${friendName} 发送消息: ${message}`); appendLog('send_success', friendName, message); } else { console.log(`给 ${friendName} 发送消息失败`); appendLog('send_fail', friendName, ''); } } return success; } function showConfigPanel() { const existingPanel = document.querySelector('.douyin-chat-config-panel'); if (existingPanel) { existingPanel.remove(); } const panel = document.createElement('div'); panel.className = 'douyin-chat-config-panel'; panel.style.cssText = ` position: fixed; top: 100px; right: 20px; background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(221, 221, 221, 0.7); border-radius: 8px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 9999; width: 320px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; backdrop-filter: blur(10px); `; panel.innerHTML = `

抖音私信自动发送

`; document.body.appendChild(panel); document.getElementById('friendList').value = CONFIG.targetFriends.join('\n'); const savedSendType = GM_getValue(SEND_TYPE_KEY, 'text'); const sendTypeRadio = document.querySelector(`input[name="sendType"][value="${savedSendType}"]`); if (sendTypeRadio) { sendTypeRadio.checked = true; } document.getElementById('monitorServer').value = CONFIG.monitorServer; document.getElementById('saveBtn').addEventListener('click', () => { const friendList = document.getElementById('friendList').value; const monitorServerVal = document.getElementById('monitorServer').value; const status = document.getElementById('status'); const friends = friendList.split('\n').map(f => f.trim()).filter(f => f); CONFIG.targetFriends = friends; CONFIG.monitorServer = monitorServerVal; GM_setValue(CONFIG_KEY, friends); GM_setValue(MONITOR_SERVER_KEY, monitorServerVal); startMonitorReport(); status.innerHTML = '设置已保存'; }); document.getElementById('startBtn').addEventListener('click', async () => { const friendList = document.getElementById('friendList').value; const customMsg = document.getElementById('customMessage').value; const sendType = document.querySelector('input[name="sendType"]:checked').value; const status = document.getElementById('status'); if (!friendList.trim()) { status.innerHTML = '请输入好友列表'; return; } const friends = friendList.split('\n').map(f => f.trim()).filter(f => f); CONFIG.targetFriends = friends; CONFIG.sendType = sendType; GM_setValue(CONFIG_KEY, friends); GM_setValue(SEND_TYPE_KEY, sendType); panel.remove(); for (let i = 0; i < friends.length; i++) { console.log(`正在发送给: ${friends[i]}...`); const success = await processFriend(friends[i], customMsg, sendType); if (success) { console.log(`成功发送给: ${friends[i]}`); } else { console.log(`发送失败: ${friends[i]}`); } if (i < friends.length - 1) { await new Promise(resolve => setTimeout(resolve, CONFIG.intervalDelay)); } } console.log('=== 发送完成 ==='); }); document.getElementById('closeBtn').addEventListener('click', () => { panel.remove(); }); const savedScheduleTime = GM_getValue(SCHEDULE_TIME_KEY, ''); document.getElementById('scheduleTime').value = savedScheduleTime; // 更新定时状态显示 function updateScheduleStatusDisplay() { const scheduleStatusDiv = document.getElementById('scheduleStatus'); const scheduleBtn = document.getElementById('scheduleBtn'); let scheduleStatus = null; try { scheduleStatus = JSON.parse(localStorage.getItem(SCHEDULE_STATUS_KEY) || 'null'); } catch (e) {} if (scheduleStatus && scheduleStatus.status === 'active' && scheduleStatus.time) { scheduleStatusDiv.innerHTML = `定时发送运行中: ${scheduleStatus.time} (北京时间)`; scheduleBtn.disabled = true; scheduleBtn.style.opacity = '0.5'; scheduleBtn.style.cursor = 'not-allowed'; } else { scheduleStatusDiv.innerHTML = '定时已关闭'; scheduleBtn.disabled = false; scheduleBtn.style.opacity = ''; scheduleBtn.style.cursor = ''; } } updateScheduleStatusDisplay(); document.getElementById('scheduleBtn').addEventListener('click', () => { const scheduleTime = document.getElementById('scheduleTime').value; const friendList = document.getElementById('friendList').value; const sendType = document.querySelector('input[name="sendType"]:checked').value; const monitorServerVal = document.getElementById('monitorServer').value; const status = document.getElementById('status'); if (!scheduleTime) { status.innerHTML = '请设置定时时间'; return; } if (!friendList.trim()) { status.innerHTML = '请输入好友列表'; return; } const friends = friendList.split('\n').map(f => f.trim()).filter(f => f); CONFIG.targetFriends = friends; CONFIG.sendType = sendType; CONFIG.scheduleTime = scheduleTime; CONFIG.monitorServer = monitorServerVal; GM_setValue(CONFIG_KEY, friends); GM_setValue(SEND_TYPE_KEY, sendType); GM_setValue(SCHEDULE_TIME_KEY, scheduleTime); GM_setValue(MONITOR_SERVER_KEY, monitorServerVal); startSchedule(scheduleTime); startMonitorReport(); status.innerHTML = `定时发送已开启: 每天 ${scheduleTime} (北京时间)`; updateScheduleStatusDisplay(); }); document.getElementById('stopScheduleBtn').addEventListener('click', () => { stopSchedule(); const status = document.getElementById('status'); status.innerHTML = '定时发送已关闭'; updateScheduleStatusDisplay(); }); } function makeButtonDraggable(button) { let isDragging = false; let hasMoved = false; let startX, startY, initialLeft, initialTop; button.addEventListener('mousedown', (e) => { isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; initialLeft = button.offsetLeft; initialTop = button.offsetTop; button.style.right = 'auto'; button.style.left = `${initialLeft}px`; button.style.cursor = 'grabbing'; button.style.zIndex = '9999'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { hasMoved = true; } button.style.left = `${initialLeft + dx}px`; button.style.top = `${initialTop + dy}px`; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; button.style.cursor = 'grab'; button.style.zIndex = '9998'; } }); return { get hasMoved() { return hasMoved; } }; } function showMonitorPanel() { const existing = document.querySelector('.douyin-chat-monitor-panel'); if (existing) { existing.remove(); return; } const panel = document.createElement('div'); panel.className = 'douyin-chat-monitor-panel'; panel.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(15, 15, 20, 0.95); backdrop-filter: blur(10px); z-index: 10000; display: flex; justify-content: center; align-items: flex-start; padding-top: 40px; overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; `; panel.innerHTML = `

抖音私信 · 监控面板

上次刷新:--
📊状态概览
加载中...
👥好友列表
加载中...
📋发送日志
加载中...
`; document.body.appendChild(panel); document.getElementById('monitorCloseBtn').addEventListener('click', () => { if (monitorRefreshTimer) { clearInterval(monitorRefreshTimer); monitorRefreshTimer = null; } panel.remove(); }); document.getElementById('monitorRefreshBtn').addEventListener('click', refreshMonitorData); let monitorRefreshTimer = null; function refreshMonitorData() { let scheduleStatus = null; try { scheduleStatus = JSON.parse(localStorage.getItem(SCHEDULE_STATUS_KEY) || 'null'); } catch (e) {} if (!scheduleStatus) { const t = GM_getValue(SCHEDULE_TIME_KEY, ''); if (t) { scheduleStatus = { status: 'active', time: t }; } } const lastSentDate = getLastSentDate(); const friends = CONFIG.targetFriends; let scheduleTime = ''; if (scheduleStatus && scheduleStatus.time) { scheduleTime = scheduleStatus.time; } else { scheduleTime = GM_getValue(SCHEDULE_TIME_KEY, ''); } let logs = []; try { logs = JSON.parse(localStorage.getItem(LOG_KEY) || '[]'); } catch (e) {} renderStatusOverview(scheduleStatus, lastSentDate, scheduleTime); renderFriendsList(friends); renderLogs(logs); const now = new Date(); const pad = (n) => n < 10 ? '0' + n : n; document.getElementById('monitorRefreshInfo').textContent = '上次刷新:' + pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()); } function renderStatusOverview(scheduleStatus, lastSentDate, scheduleTime) { const container = document.getElementById('monitorStatusOverview'); let statusText = '未知'; let statusClass = ''; let dotClass = ''; if (scheduleStatus && scheduleStatus.status === 'active') { statusText = '运行中'; statusClass = 'running'; dotClass = 'running'; } else if (scheduleStatus && scheduleStatus.status === 'inactive') { statusText = '已关闭'; statusClass = 'stopped'; dotClass = 'stopped'; } else { statusText = '未设置'; statusClass = 'stopped'; dotClass = 'stopped'; } const today = getTodayDateStr(); const sentToday = (lastSentDate && lastSentDate === today) ? '是' : '否'; const sentColor = sentToday === '是' ? '#22c55e' : '#f59e0b'; const statusColor = statusClass === 'running' ? '#22c55e' : '#6b7280'; const pulseStyle = statusClass === 'running' ? 'animation: pulse 2s infinite;' : ''; container.innerHTML = `
定时状态
${statusText}
定时时间
${scheduleTime || '未设置'}
上次发送日期
${lastSentDate || '--'}
当天已发送
${sentToday}
`; } function renderFriendsList(friends) { const container = document.getElementById('monitorFriendsList'); if (!friends || !Array.isArray(friends) || friends.length === 0) { container.innerHTML = '
暂无数据
'; return; } let html = ''; friends.forEach((f) => { const name = String(f); html += `${escapeHtml(name)}`; }); container.innerHTML = html; } function renderLogs(logs) { const container = document.getElementById('monitorLogsTable'); if (!logs || !Array.isArray(logs) || logs.length === 0) { container.innerHTML = '
暂无数据
'; return; } const typeMap = { 'send_success': '发送成功', 'send_fail': '发送失败', 'schedule_trigger': '定时触发', 'error': '错误' }; const typeColors = { 'send_success': 'rgba(34, 197, 94, 0.15); color: #22c55e', 'send_fail': 'rgba(239, 68, 68, 0.15); color: #ef4444', 'schedule_trigger': 'rgba(59, 130, 246, 0.15); color: #3b82f6', 'error': 'rgba(249, 115, 22, 0.15); color: #f97316' }; let html = ''; html += ''; html += ''; html += ''; html += ''; html += ''; const reversed = logs.slice().reverse(); reversed.forEach((log, idx) => { const time = formatLogTime(log.time); const type = log.type || ''; const typeLabel = typeMap[type] || type; const typeColor = typeColors[type] || 'rgba(107, 114, 128, 0.15); color: #9ca3af'; const friend = log.friend || '--'; const content = log.message || '--'; const bgColor = idx % 2 === 0 ? '' : 'background: rgba(255, 255, 255, 0.02);'; html += ``; html += ``; html += ``; html += ``; html += ``; html += ''; }); html += '
时间类型好友内容
${time}${typeLabel}${escapeHtml(friend)}${escapeHtml(truncateStr(content, 80))}
'; container.innerHTML = html; } function formatLogTime(ts) { if (!ts) return '--'; const d = new Date(ts); const pad = (n) => n < 10 ? '0' + n : n; return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); } function escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function truncateStr(str, len) { return String(str).length > len ? String(str).slice(0, len) + '...' : str; } refreshMonitorData(); monitorRefreshTimer = setInterval(refreshMonitorData, 30000); } const button = document.createElement('button'); button.style.cssText = ` position: fixed; top: 20px; right: 20px; left: auto; background: #fe2c55; color: white; border: none; border-radius: 20px; padding: 10px 20px; font-size: 14px; cursor: grab; z-index: 9998; box-shadow: 0 2px 8px rgba(254,44,85,0.4); user-select: none; -webkit-user-select: none; `; button.textContent = '私信发送'; const dragState = makeButtonDraggable(button); button.addEventListener('click', (e) => { if (!dragState.hasMoved) { const existingPanel = document.querySelector('.douyin-chat-config-panel'); if (existingPanel) { existingPanel.remove(); } else { showConfigPanel(); } } }); document.body.appendChild(button); const monitorBtn = document.createElement('button'); monitorBtn.style.cssText = ` position: fixed; top: 70px; right: 20px; left: auto; background: linear-gradient(135deg, #6366f1, #a855f7); color: white; border: none; border-radius: 20px; padding: 10px 20px; font-size: 14px; cursor: grab; z-index: 9998; box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4); user-select: none; -webkit-user-select: none; `; monitorBtn.textContent = '监控面板'; const monitorDragState = makeButtonDraggable(monitorBtn); monitorBtn.addEventListener('click', (e) => { if (!monitorDragState.hasMoved) { showMonitorPanel(); } }); document.body.appendChild(monitorBtn); // 页面加载时恢复定时任务:优先从 localStorage 读取状态 let restoredScheduleTime = null; try { const savedStatus = JSON.parse(localStorage.getItem(SCHEDULE_STATUS_KEY) || 'null'); if (savedStatus && savedStatus.status === 'active' && savedStatus.time) { restoredScheduleTime = savedStatus.time; } } catch (e) {} // 回退:从 GM_getValue 读取 if (!restoredScheduleTime) { restoredScheduleTime = GM_getValue(SCHEDULE_TIME_KEY, ''); } if (restoredScheduleTime) { startSchedule(restoredScheduleTime); console.log('页面加载时恢复定时任务:', restoredScheduleTime); } startRefreshTimer(); if (CONFIG.monitorServer) { startMonitorReport(); console.log('监控数据上报已启动:', CONFIG.monitorServer); } console.log('抖音私信自动发送脚本已加载'); })();