// ==UserScript== // @name Xubai:B站直播间弹幕自动发送 // @namespace http://tampermonkey.net/ // @author Xubai0224 // @version 1.6.4 // @description B站直播间自动发送弹幕,支持计时/计数模式、随机间隔、去重随机选择 // @grant GM_addStyle // @grant GM_addElement // @include https://live.bilibili.com/* // @match *://live.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @icon64 https://www.bilibili.com/favicon.ico // @tag BiliBili // @license MIT // ==/UserScript== (function() { 'use strict'; // ========================================== // 配置常量 - 可根据需求调整基础参数 // ========================================== const CONFIG = { minInterval: 20, // 最小发送间隔(秒) maxInterval: 25, // 最大发送间隔(秒) maxDurationHour: 8, // 计时模式最大小时数 maxDurationMinute: 59, // 计时模式最大分钟数 maxSingleCount: 10000 // 计数模式最大单次弹幕数 }; // 面板样式配置 const PANEL_STYLES = { // 控制面板样式(开始/停止按钮所在面板) control: { position: 'fixed', top: '80px', left: '24px', width: '200px', height: '140px', zIndex: '9999999', borderRadius: '12px', boxShadow: '0 4px 15px rgba(64, 158, 255, 0.2)', margin: '0', padding: '0', backgroundColor: '#ffffff', overflow: 'hidden', transition: 'box-shadow 0.3s ease' }, // 配置面板样式 config: { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '380px', height: '450px', zIndex: '99999999', borderRadius: '12px', boxShadow: '0 8px 30px rgba(0, 0, 0, 0.15)', margin: '0', padding: '20px', backgroundColor: '#ffffff', display: 'none', flexDirection: 'column', border: '1px solid #f0f0f0' } }; // ========================================== // 全局状态管理 // ========================================== const state = { // DOM元素引用 elements: { controlPanel: null, titleBar: null, countDisplay: null, controlButton: null, configPanel: null, modeRadios: { timer: null, count: null }, durationHourInput: null, durationMinuteInput: null, countInput: null, messageInput: null, confirmBtn: null, cancelBtn: null, chatInput: null, sendButton: null }, // 拖动相关状态 drag: { isDragging: false, startX: 0, startY: 0, startLeft: 0, startTop: 0 }, // 发送相关状态 sending: { isActive: false, timeout: null, lastMessageIndex: -1, startTime: 0 }, // 当前发送配置 currentConfig: { mode: '', // timer/count duration: 0, // 秒 totalCount: 0, // 条 messageList: [], // 弹幕列表 sentCount: 0 // 已发送数 } }; // ========================================== // 工具函数 // ========================================== /** * 生成随机间隔时间(毫秒) * @returns {number} 随机间隔时间 */ function getRandomInterval() { return Math.random() * (CONFIG.maxInterval - CONFIG.minInterval) * 1000 + CONFIG.minInterval * 1000; } /** * 随机选择一条弹幕,与上一条不重复 * @param {Array} messageList - 弹幕列表 * @returns {number} 选中的索引 */ function getRandomMessageIndex(messageList) { if (messageList.length <= 1) return 0; let randomIndex; do { randomIndex = Math.floor(Math.random() * messageList.length); } while (randomIndex === state.sending.lastMessageIndex); state.sending.lastMessageIndex = randomIndex; return randomIndex; } /** * 模拟点击事件 * @param {HTMLElement} element - 目标元素 */ function simulateClick(element) { const events = ['mousedown', 'mouseup', 'click']; events.forEach(eventType => { element.dispatchEvent(new MouseEvent(eventType, { bubbles: true, cancelable: true, view: document.defaultView })); }); } // ========================================== // 面板创建与初始化 // ========================================== /** * 初始化控制面板(开始/停止按钮所在面板) */ function initControlPanel() { // 创建控制面板容器 state.elements.controlPanel = document.createElement('div'); Object.assign(state.elements.controlPanel.style, PANEL_STYLES.control); // 鼠标悬停效果 state.elements.controlPanel.addEventListener('mouseenter', () => { state.elements.controlPanel.style.boxShadow = '0 6px 20px rgba(64, 158, 255, 0.3)'; }); state.elements.controlPanel.addEventListener('mouseleave', () => { state.elements.controlPanel.style.boxShadow = '0 4px 15px rgba(64, 158, 255, 0.2)'; }); document.body.appendChild(state.elements.controlPanel); // 创建面板组件 createTitleBar(); createCountDisplay(); createControlButton(); } /** * 创建可拖动的标题栏 */ function createTitleBar() { state.elements.titleBar = document.createElement('div'); state.elements.titleBar.innerText = '弹幕自动发送'; Object.assign(state.elements.titleBar.style, { backgroundColor: '#409EFF', color: '#ffffff', fontWeight: '600', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '36px', margin: '0', padding: '0', borderRadius: '12px 12px 0 0', cursor: 'grab', boxShadow: '0 2px 5px rgba(0, 0, 0, 0.1)' }); // 绑定拖动事件 state.elements.titleBar.addEventListener('mousedown', startDrag); state.elements.controlPanel.appendChild(state.elements.titleBar); } /** * 创建发送计数显示区域 */ function createCountDisplay() { state.elements.countDisplay = document.createElement('div'); state.elements.countDisplay.innerText = `本次已发送: 0 条`; Object.assign(state.elements.countDisplay.style, { color: '#606266', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '44px', margin: '0', padding: '0 10px', fontSize: '12px', borderBottom: '1px solid #f5f5f5', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }); state.elements.controlPanel.appendChild(state.elements.countDisplay); } /** * 创建开始/停止控制按钮 */ function createControlButton() { state.elements.controlButton = document.createElement('button'); state.elements.controlButton.innerText = '开始发送'; Object.assign(state.elements.controlButton.style, { margin: '15px auto 0', width: 'calc(100% - 30px)', backgroundColor: '#409EFF', borderRadius: '6px', outline: 'none', border: 'none', color: 'white', fontWeight: '600', padding: '8px 0', cursor: 'pointer', fontSize: '15px', display: 'block', transition: 'all 0.2s ease', boxShadow: '0 2px 5px rgba(64, 158, 255, 0.3)' }); // 按钮交互效果 state.elements.controlButton.addEventListener('mouseenter', () => { state.elements.controlButton.style.backgroundColor = '#66b1ff'; state.elements.controlButton.style.transform = 'translateY(-1px)'; }); state.elements.controlButton.addEventListener('mouseleave', () => { state.elements.controlButton.style.backgroundColor = state.sending.isActive ? '#f56c6c' : '#409EFF'; state.elements.controlButton.style.transform = 'translateY(0)'; }); state.elements.controlButton.addEventListener('mousedown', () => { state.elements.controlButton.style.transform = 'translateY(1px)'; }); state.elements.controlButton.addEventListener('mouseup', () => { state.elements.controlButton.style.transform = 'translateY(0)'; }); // 绑定点击事件 state.elements.controlButton.addEventListener('click', toggleSend); state.elements.controlPanel.appendChild(state.elements.controlButton); } /** * 初始化配置面板 */ function initConfigPanel() { // 创建配置面板容器 state.elements.configPanel = document.createElement('div'); Object.assign(state.elements.configPanel.style, PANEL_STYLES.config); document.body.appendChild(state.elements.configPanel); // 创建面板标题 const configTitle = document.createElement('h3'); configTitle.innerText = '弹幕发送配置'; configTitle.style = ` margin: 0 0 25px 0; color: #303133; text-align: center; font-size: 18px; font-weight: 600; `; state.elements.configPanel.appendChild(configTitle); // 创建模式选择区域 createModeSelection(); // 创建弹幕内容输入区域 createMessageInput(); // 创建按钮组 createConfigButtons(); } /** * 创建模式选择区域(计时/计数) */ function createModeSelection() { const modeGroup = document.createElement('div'); modeGroup.style.marginBottom = '25px'; state.elements.configPanel.appendChild(modeGroup); // 模式选择标题 const modeLabel = document.createElement('label'); modeLabel.innerText = '限制模式:'; modeLabel.style = ` display: block; margin-bottom: 10px; color: #606266; font-size: 14px; `; modeGroup.appendChild(modeLabel); // 计时模式 const timerDiv = document.createElement('div'); timerDiv.style = 'margin-bottom: 10px; display: flex; align-items: center;'; modeGroup.appendChild(timerDiv); state.elements.modeRadios.timer = document.createElement('input'); state.elements.modeRadios.timer.type = 'radio'; state.elements.modeRadios.timer.name = 'limitMode'; state.elements.modeRadios.timer.value = 'timer'; state.elements.modeRadios.timer.id = 'timerMode'; state.elements.modeRadios.timer.style = 'width: 16px; height: 16px; margin: 0;'; timerDiv.appendChild(state.elements.modeRadios.timer); const timerLabel = document.createElement('label'); timerLabel.htmlFor = 'timerMode'; timerLabel.innerText = `计时模式(最长${CONFIG.maxDurationHour}小时${CONFIG.maxDurationMinute}分钟)`; timerLabel.style = 'margin-left: 8px; color: #606266; font-size: 14px;'; timerDiv.appendChild(timerLabel); // 计时输入框容器 const durationContainer = document.createElement('div'); durationContainer.style = 'display: flex; gap: 15px; margin: 0 0 20px 0; display: none;'; state.elements.configPanel.appendChild(durationContainer); // 小时输入 state.elements.durationHourInput = createNumberInput({ placeholder: '小时', min: 0, max: CONFIG.maxDurationHour, style: 'width: calc(50% - 15px);' }); durationContainer.appendChild(state.elements.durationHourInput); // 分钟输入 state.elements.durationMinuteInput = createNumberInput({ placeholder: '分钟', min: 0, max: CONFIG.maxDurationMinute, style: 'width: calc(50% - 15px);' }); durationContainer.appendChild(state.elements.durationMinuteInput); // 计数模式 const countDiv = document.createElement('div'); countDiv.style = 'display: flex; align-items: center;'; modeGroup.appendChild(countDiv); state.elements.modeRadios.count = document.createElement('input'); state.elements.modeRadios.count.type = 'radio'; state.elements.modeRadios.count.name = 'limitMode'; state.elements.modeRadios.count.value = 'count'; state.elements.modeRadios.count.id = 'countMode'; state.elements.modeRadios.count.style = 'width: 16px; height: 16px; margin: 0;'; countDiv.appendChild(state.elements.modeRadios.count); const countLabel = document.createElement('label'); countLabel.htmlFor = 'countMode'; countLabel.innerText = `计数模式(最多${CONFIG.maxSingleCount}条)`; countLabel.style = 'margin-left: 8px; color: #606266; font-size: 14px;'; countDiv.appendChild(countLabel); // 计数输入框 state.elements.countInput = createNumberInput({ placeholder: '请输入发送总数(条)', min: 1, max: CONFIG.maxSingleCount, style: 'width: calc(100% - 24px); margin: 0 0 20px 0; display: none;' }); state.elements.configPanel.appendChild(state.elements.countInput); // 模式切换事件 state.elements.modeRadios.timer.addEventListener('change', () => { durationContainer.style.display = 'flex'; state.elements.countInput.style.display = 'none'; }); state.elements.modeRadios.count.addEventListener('change', () => { durationContainer.style.display = 'none'; state.elements.countInput.style.display = 'block'; }); } /** * 创建数字输入框 * @param {Object} options - 输入框配置 * @returns {HTMLInputElement} 创建的输入框元素 */ function createNumberInput(options) { const input = document.createElement('input'); input.type = 'number'; input.placeholder = options.placeholder; input.min = options.min; input.max = options.max; input.step = '1'; input.style = ` ${options.style} height: 36px; padding: 0 12px; border: 1px solid #dcdfe6; border-radius: 6px; font-size: 14px; transition: border-color 0.2s ease; `; // 焦点样式 input.addEventListener('focus', () => { input.style.borderColor = '#409EFF'; input.style.outline = 'none'; }); input.addEventListener('blur', () => { input.style.borderColor = '#dcdfe6'; }); return input; } /** * 创建弹幕内容输入区域 */ function createMessageInput() { const messageLabel = document.createElement('label'); messageLabel.innerText = '弹幕内容(多条用 ; 隔开):'; messageLabel.style = ` display: block; margin-bottom: 10px; color: #606266; font-size: 14px; `; state.elements.configPanel.appendChild(messageLabel); state.elements.messageInput = document.createElement('textarea'); state.elements.messageInput.placeholder = '例如:666;主播加油;打卡打卡'; state.elements.messageInput.value = ''; state.elements.messageInput.style = ` width: calc(100% - 24px); height: 130px; padding: 12px; margin: 0 0 25px 0; border: 1px solid #dcdfe6; border-radius: 6px; resize: none; font-size: 14px; line-height: 1.5; transition: border-color 0.2s ease; `; // 焦点样式 state.elements.messageInput.addEventListener('focus', () => { state.elements.messageInput.style.borderColor = '#409EFF'; state.elements.messageInput.style.outline = 'none'; }); state.elements.messageInput.addEventListener('blur', () => { state.elements.messageInput.style.borderColor = '#dcdfe6'; }); state.elements.configPanel.appendChild(state.elements.messageInput); } /** * 创建配置面板的确认/取消按钮 */ function createConfigButtons() { const btnGroup = document.createElement('div'); btnGroup.style = 'display: flex; justify-content: center; gap: 20px;'; state.elements.configPanel.appendChild(btnGroup); // 确认按钮 state.elements.confirmBtn = createButton({ text: '确认', style: ` background-color: #409EFF; box-shadow: 0 2px 5px rgba(64, 158, 255, 0.3); `, clickHandler: confirmConfig }); btnGroup.appendChild(state.elements.confirmBtn); // 取消按钮 state.elements.cancelBtn = createButton({ text: '取消', style: ` background-color: #f5f7fa; border: 1px solid #e4e7ed; color: #606266; `, clickHandler: cancelConfig }); btnGroup.appendChild(state.elements.cancelBtn); } /** * 创建通用按钮 * @param {Object} options - 按钮配置 * @returns {HTMLButtonElement} 创建的按钮元素 */ function createButton(options) { const button = document.createElement('button'); button.innerText = options.text; button.style = ` ${options.style} border-radius: 6px; outline: none; font-weight: 600; padding: 9px 30px; cursor: pointer; font-size: 14px; transition: all 0.2s ease; `; // 按钮交互效果 button.addEventListener('mouseenter', () => { button.style.transform = 'translateY(-1px)'; if (options.text === '确认') { button.style.backgroundColor = '#66b1ff'; } else { button.style.backgroundColor = '#e9e9eb'; } }); button.addEventListener('mouseleave', () => { button.style.transform = 'translateY(0)'; if (options.text === '确认') { button.style.backgroundColor = '#409EFF'; } else { button.style.backgroundColor = '#f5f7fa'; } }); button.addEventListener('mousedown', () => { button.style.transform = 'translateY(1px)'; }); button.addEventListener('mouseup', () => { button.style.transform = 'translateY(0)'; }); // 绑定点击事件 button.addEventListener('click', options.clickHandler); return button; } // ========================================== // 拖动相关功能 // ========================================== /** * 开始拖动面板 * @param {MouseEvent} e - 鼠标事件 */ function startDrag(e) { state.elements.titleBar.style.cursor = 'grabbing'; state.drag.isDragging = true; state.drag.startX = e.clientX; state.drag.startY = e.clientY; state.drag.startLeft = parseInt(window.getComputedStyle(state.elements.controlPanel).left, 10); state.drag.startTop = parseInt(window.getComputedStyle(state.elements.controlPanel).top, 10); document.addEventListener('mousemove', handleDrag); document.addEventListener('mouseup', stopDrag); } /** * 处理拖动逻辑 * @param {MouseEvent} e - 鼠标事件 */ function handleDrag(e) { if (!state.drag.isDragging) return; const dx = e.clientX - state.drag.startX; const dy = e.clientY - state.drag.startY; state.elements.controlPanel.style.left = `${state.drag.startLeft + dx}px`; state.elements.controlPanel.style.top = `${state.drag.startTop + dy}px`; } /** * 停止拖动 */ function stopDrag() { state.elements.titleBar.style.cursor = 'grab'; state.drag.isDragging = false; document.removeEventListener('mousemove', handleDrag); document.removeEventListener('mouseup', stopDrag); } // ========================================== // 发送控制功能 // ========================================== /** * 切换发送状态(开始/停止) */ function toggleSend() { if (!state.sending.isActive) { // 显示配置面板 state.elements.configPanel.style.display = 'flex'; } else { // 停止发送 stopSending(); } } /** * 确认配置并开始发送 */ function confirmConfig() { // 验证模式选择 let selectedMode = ''; if (state.elements.modeRadios.timer.checked) { selectedMode = 'timer'; } else if (state.elements.modeRadios.count.checked) { selectedMode = 'count'; } if (!selectedMode) { alert('请选择限制模式(计时/计数)'); return; } // 验证弹幕内容 const messageText = state.elements.messageInput.value.trim(); if (!messageText) { alert('请输入弹幕内容'); return; } const messageList = messageText.split(';').filter(item => item.trim() !== ''); if (messageList.length === 0) { alert('请输入有效的弹幕内容(不能全为空)'); return; } // 验证并保存模式配置 if (selectedMode === 'timer') { if (!validateAndSetTimerConfig(messageList)) return; } else { if (!validateAndSetCountConfig(messageList)) return; } // 隐藏配置面板并开始发送 state.elements.configPanel.style.display = 'none'; startSending(); } /** * 验证并设置计时模式配置 * @param {Array} messageList - 弹幕列表 * @returns {boolean} 是否验证通过 */ function validateAndSetTimerConfig(messageList) { const hours = parseInt(state.elements.durationHourInput.value, 10); const minutes = parseInt(state.elements.durationMinuteInput.value, 10); // 检查是否为有效数字 if (isNaN(hours) || isNaN(minutes)) { alert('请输入有效的小时和分钟数'); return false; } // 检查范围 if (hours < 0 || hours > CONFIG.maxDurationHour) { alert(`小时数必须在0-${CONFIG.maxDurationHour}之间`); return false; } if (minutes < 0 || minutes > CONFIG.maxDurationMinute) { alert(`分钟数必须在0-${CONFIG.maxDurationMinute}之间`); return false; } // 检查总时长不能为0 if (hours === 0 && minutes === 0) { alert('总时长不能为0'); return false; } // 保存配置 state.currentConfig = { mode: 'timer', duration: hours * 3600 + minutes * 60, totalCount: 0, messageList: messageList, sentCount: 0 }; state.sending.startTime = Date.now(); return true; } /** * 验证并设置计数模式配置 * @param {Array} messageList - 弹幕列表 * @returns {boolean} 是否验证通过 */ function validateAndSetCountConfig(messageList) { const count = parseInt(state.elements.countInput.value, 10); if (isNaN(count) || count < 1 || count > CONFIG.maxSingleCount) { alert(`请输入有效的发送数量(1-${CONFIG.maxSingleCount}条)`); return false; } // 保存配置 state.currentConfig = { mode: 'count', duration: 0, totalCount: count, messageList: messageList, sentCount: 0 }; return true; } /** * 取消配置 */ function cancelConfig() { state.elements.configPanel.style.display = 'none'; } /** * 增加弹幕计数并更新显示 */ function incrementMessageCount() { state.currentConfig.sentCount++; state.elements.countDisplay.innerText = `本次已发送: ${state.currentConfig.sentCount} 条`; } /** * 开始发送弹幕 */ function startSending() { console.log('开始发送弹幕'); state.sending.isActive = true; state.elements.controlButton.innerText = '停止发送'; state.elements.controlButton.style.backgroundColor = '#f56c6c'; state.elements.controlButton.style.boxShadow = '0 2px 5px rgba(245, 108, 108, 0.3)'; // 发送下一条消息 function sendNextMessage() { if (!state.sending.isActive) return; // 检查模式限制 if (state.currentConfig.mode === 'timer') { const elapsed = (Date.now() - state.sending.startTime) / 1000; if (elapsed >= state.currentConfig.duration) { const hours = Math.floor(state.currentConfig.duration / 3600); const minutes = Math.floor((state.currentConfig.duration % 3600) / 60); alert(`已达到设定的${hours}小时${minutes}分钟发送时长,停止发送`); stopSending(); return; } } else { if (state.currentConfig.sentCount >= state.currentConfig.totalCount) { alert(`已发送完设定的${state.currentConfig.totalCount}条弹幕,停止发送`); stopSending(); return; } } // 随机选择一条弹幕并发送 const currentIndex = getRandomMessageIndex(state.currentConfig.messageList); if (state.elements.chatInput && state.elements.sendButton) { state.elements.chatInput.focus(); state.elements.chatInput.value = state.currentConfig.messageList[currentIndex]; state.elements.chatInput.dispatchEvent(new Event('input', { bubbles: true })); // 模拟发送按钮点击 simulateClick(state.elements.sendButton); console.log(`发送弹幕: ${state.currentConfig.messageList[currentIndex]}`); // 更新计数 incrementMessageCount(); } // 预约下一次发送 const nextInterval = getRandomInterval(); state.sending.timeout = setTimeout(sendNextMessage, nextInterval); } // 立即发送第一条 sendNextMessage(); } /** * 停止发送弹幕 */ function stopSending() { console.log('停止发送弹幕'); if (state.sending.timeout) { clearTimeout(state.sending.timeout); } state.sending.isActive = false; state.elements.controlButton.innerText = '开始发送'; state.elements.controlButton.style.backgroundColor = '#409EFF'; state.elements.controlButton.style.boxShadow = '0 2px 5px rgba(64, 158, 255, 0.3)'; } // ========================================== // 初始化与启动 // ========================================== /** * 初始化:检查并获取弹幕输入框和发送按钮 */ function init() { // 先初始化配置面板 initConfigPanel(); // 定期检查弹幕输入框和发送按钮是否加载完成 const checkInterval = setInterval(() => { state.elements.chatInput = document.querySelector('textarea.chat-input.border-box'); state.elements.sendButton = document.querySelector( '.bl-button.live-skin-highlight-button-bg.live-skin-button-text.bl-button--primary.bl-button--small' ); if (state.elements.chatInput && state.elements.sendButton) { console.log('✔️ 找到输入框和发送按钮,初始化控制面板'); clearInterval(checkInterval); initControlPanel(); } else { console.log('⌛ 等待找到输入框和按钮...'); } }, 3000); } // 启动脚本 console.log('B站直播间弹幕自动发送脚本启动'); init(); })();