// ==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 = `
| 时间 | '; html += '类型 | '; html += '好友 | '; html += '内容 | '; html += '
|---|---|---|---|
| ${time} | `; html += `${typeLabel} | `; html += `${escapeHtml(friend)} | `; html += ``; html += '