// ==UserScript==
// @name 抖音直播间自动弹幕 (纯净事件兼容版)
// @namespace douyin-auto-danmaku
// @version 2.1.0
// @description 攻克 Failed to construct 'MouseEvent' 报错,采用无视窗口状态的老牌底层事件派发技术
// @author 小德捡银子
// @match https://live.douyin.com/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// @license MIT
// @tag 抖音
// @tag 弹幕助手
// @tag 抖音直播
// ==/UserScript==
(function () {
'use strict';
// ========== 配置 ==========
const MAX_LOG_COUNT = 200;
// ========== 状态 ==========
let timer = null;
let currentIndex = 0;
let stats = { total: 0, success: 0, fail: 0 };
let danmakuList = GM_getValue('danmakuList', ['大家好', '主播加油', '666']);
let interval = GM_getValue('interval', 10);
// ========== UI 构建 ==========
const panel = document.createElement('div');
panel.id = 'dy-danmaku-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
const $body = document.getElementById('dp-body'), $toggle = document.getElementById('dp-toggle'), $list = document.getElementById('dp-list'), $newText = document.getElementById('dp-new-text'), $addBtn = document.getElementById('dp-add-btn'), $intervalInput = document.getElementById('dp-interval'), $startBtn = document.getElementById('dp-start-btn'), $sTotal = document.getElementById('dp-s-total'), $sOk = document.getElementById('dp-s-ok'), $sFail = document.getElementById('dp-s-fail'), $log = document.getElementById('dp-log');
function timeStr() { const d = new Date(); return [d.getHours(), d.getMinutes(), d.getSeconds()].map(v => String(v).padStart(2, '0')).join(':'); }
function addLog(msg, type = 'ok') {
const line = document.createElement('div'); line.innerHTML = `[${timeStr()}] ${msg}`;
$log.appendChild(line); while ($log.children.length > MAX_LOG_COUNT) { $log.removeChild($log.firstChild); } $log.scrollTop = $log.scrollHeight;
}
function updateStats() { $sTotal.textContent = stats.total; $sOk.textContent = stats.success; $sFail.textContent = stats.fail; }
function saveDanmakuList() { GM_setValue('danmakuList', danmakuList); }
function saveInterval() { GM_setValue('interval', interval); }
function renderList() {
$list.innerHTML = ''; if (danmakuList.length === 0) { $list.innerHTML = '暂无弹幕
'; return; }
danmakuList.forEach((text, i) => {
const item = document.createElement('div'); item.className = 'dp-item'; item.innerHTML = `${i + 1}. ${text}×`; $list.appendChild(item);
});
}
// ========== 发送核心逻辑 ==========
function sendDanmaku(text) {
try {
const input = document.querySelector('#chatInput [contenteditable="true"]') || document.querySelector('[data-slate-editor="true"]');
if (!input) { addLog('错误: 未找到输入框', 'err'); stats.total++; stats.fail++; updateStats(); return; }
// 1. 点亮高亮输入
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(input);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('delete', false, null);
document.execCommand('insertText', false, text);
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
// 2. 延迟 150ms 后触发不依赖 window/view 状态的老牌事件派发
setTimeout(() => {
try {
const targetSVG = document.querySelector('svg.webcast-chatroom___send-btn');
if (targetSVG) {
// 使用老牌的、不受非活动窗口限制的 initEvent 机制创建纯净点击
const evt = document.createEvent('MouseEvents');
// 参数含义:事件类型(click), 是否冒泡(true), 是否可取消(true), 后面补齐不带 window 属性的默认数据
evt.initEvent('click', true, true);
// 像红包抢夺脚本那样,全方位轰炸:
// A. 点 SVG 本身
targetSVG.dispatchEvent(evt);
// B. 轰炸内部的所有 path 图层(确保穿透到位)
const paths = targetSVG.querySelectorAll('path');
paths.forEach(p => p.dispatchEvent(evt));
// C. 尝试寻找它的父级节点(通常是容纳 SVG 的包裹 div 或容器)
if (targetSVG.parentElement) {
targetSVG.parentElement.dispatchEvent(evt);
}
addLog(`成功: [纯净兼容发送] ${text}`, 'ok');
stats.success++;
} else {
addLog('错误: 后台未锁定亮起的飞机SVG图标', 'err');
stats.fail++;
}
} catch (innerE) {
addLog(`点击异常: ${innerE.message}`, 'err');
stats.fail++;
}
stats.total++;
updateStats();
}, 150);
} catch (e) {
addLog(`执行异常: ${e.message}`, 'err'); stats.total++; stats.fail++; updateStats();
}
}
// ========== 定时器 ==========
function startSending() {
if (danmakuList.length === 0) { addLog('列表为空,无法开始', 'err'); return; }
currentIndex = 0;
addLog(`挂机技术已就绪,请测试并查看最新表现...`, 'info');
sendDanmaku(danmakuList[currentIndex]);
currentIndex = (currentIndex + 1) % danmakuList.length;
timer = setInterval(() => {
if (danmakuList.length === 0) { stopSending(); return; }
sendDanmaku(danmakuList[currentIndex]);
currentIndex = (currentIndex + 1) % danmakuList.length;
}, interval * 1000);
$startBtn.textContent = '停止发送'; $startBtn.className = 'dp-btn dp-btn-stop';
}
function stopSending() {
if (timer) { clearInterval(timer); timer = null; }
addLog('系统提示: 已停止自动发送', 'info');
$startBtn.textContent = '开始发送'; $startBtn.className = 'dp-btn dp-btn-start';
}
// ========== UI 事件绑定 ==========
$toggle.addEventListener('click', () => { $body.classList.toggle('collapsed'); $toggle.textContent = $body.classList.contains('collapsed') ? '+' : '−'; });
function addDanmaku() { const text = $newText.value.trim(); if (!text) return; danmakuList.push(text); saveDanmakuList(); $newText.value = ''; renderList(); }
$addBtn.addEventListener('click', addDanmaku);
$newText.addEventListener('keydown', (e) => { if (e.key === 'Enter') addDanmaku(); });
$list.addEventListener('click', (e) => {
if (e.target.classList.contains('dp-del')) {
const idx = parseInt(e.target.dataset.index, 10); danmakuList.splice(idx, 1); saveDanmakuList(); renderList();
if (currentIndex >= danmakuList.length) currentIndex = 0;
}
});
$intervalInput.addEventListener('change', () => {
let val = parseInt($intervalInput.value, 10); if (isNaN(val) || val < 3) val = 3;
$intervalInput.value = val; interval = val; saveInterval(); if (timer) { stopSending(); startSending(); }
});
$startBtn.addEventListener('click', () => { if (timer) { stopSending(); } else { startSending(); } });
// 拖拽面板
const header = panel.querySelector('.dp-header'); let isDragging = false, dragX, dragY;
header.addEventListener('mousedown', (e) => { if (e.target.classList.contains('dp-toggle')) return; isDragging = true; dragX = e.clientX - panel.offsetLeft; dragY = e.clientY - panel.offsetTop; });
document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.left = (e.clientX - dragX) + 'px'; panel.style.top = (e.clientY - dragY) + 'px'; panel.style.right = 'auto'; });
document.addEventListener('mouseup', () => { isDragging = false; });
renderList();
})();