// ==UserScript== // @name B站评论弹幕监控助手11 // @name:zh-CN B站评论弹幕监控助手11 // @name:en Bilibili Comment & Danmaku Monitor // @namespace https://scriptcat.org/users/naaammme // @version 1.0.0 // @description 实时监控并记录B站评论和弹幕,优化版本降低风控检测风险(可能有兼容问题)。 // @author naaaammme // @match *://*.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @grant none // @run-at document-end // @license AGPL-3.0-or-later // @homepage https://github.com/naaammme/bili-monitor // @supportURL https://github.com/naaammme/bili-monitor/issues // ==/UserScript== (() => { "use strict"; let capturedComments = []; let capturedDanmaku = []; let pendingComments = new Map(); const processedRequests = new Map(); const REQUEST_CACHE_TIME = 5000; const CACHE_KEYS = { comments: 'bili_monitor_comments', danmaku: 'bili_monitor_danmaku' }; const API_CONFIG = { danmaku: ['/dm/post', '/dmpost'], comment: ['/reply/add', '/reply/post', '/v2/reply/add', '/comment/post', '/x/v2/reply/add'] }; function loadCachedData() { try { capturedComments = JSON.parse(localStorage.getItem(CACHE_KEYS.comments) || '[]'); capturedDanmaku = JSON.parse(localStorage.getItem(CACHE_KEYS.danmaku) || '[]'); console.log(`缓存加载完成 - 评论:${capturedComments.length} 弹幕:${capturedDanmaku.length}`); } catch (e) { console.error('缓存数据加载失败:', e); } } function saveToCache() { try { localStorage.setItem(CACHE_KEYS.comments, JSON.stringify(capturedComments.slice(-5000))); localStorage.setItem(CACHE_KEYS.danmaku, JSON.stringify(capturedDanmaku.slice(-5000))); } catch (e) { console.error('保存缓存失败:', e); } } function log(msg, data = null) { console.log(`[BiliMonitor ${new Date().toLocaleTimeString()}] ${msg}`, data || ''); } function generateRequestId(url, data) { const timestamp = Date.now(); const dataStr = typeof data === 'string' ? data.substring(0, 100) : ''; return `${url}-${dataStr}-${timestamp}`; } function shouldProcessRequest(requestId) { if (processedRequests.has(requestId)) { return false; } processedRequests.set(requestId, Date.now()); setTimeout(() => { processedRequests.delete(requestId); }, REQUEST_CACHE_TIME); return true; } function cleanupRequestCache() { const now = Date.now(); for (const [id, time] of processedRequests) { if (now - time > REQUEST_CACHE_TIME) { processedRequests.delete(id); } } } setInterval(cleanupRequestCache, 10000); function extractImageInfo(data) { const images = []; if (data && data.pictures) { try { let picturesData = data.pictures; if (typeof picturesData === 'string') { try { picturesData = JSON.parse(picturesData); } catch (e) { return { images: [] }; } } if (Array.isArray(picturesData)) { picturesData.forEach((img, index) => { if (img && img.img_src && typeof img.img_src === 'string') { images.push({ url: img.img_src, width: img.img_width || 0, height: img.img_height || 0, size: img.img_size || 0, index: index }); } }); } } catch (e) { console.error('解析pictures失败:', e); } } return { images }; } function createFloatingBall() { const ball = document.createElement('div'); ball.id = 'bili-monitor-ball'; ball.innerHTML = '💖'; ball.style.cssText = ` position: fixed; top: 100px; right: 20px; width: 45px; height: 45px; background: linear-gradient(135deg, #00a1d6 0%, #f25d8e 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; z-index: 999999; box-shadow: 0 2px 10px rgba(0, 161, 214, 0.3); transition: all 0.3s ease; user-select: none; `; ball.addEventListener('mouseenter', () => { ball.style.transform = 'scale(1.1)'; }); ball.addEventListener('mouseleave', () => { ball.style.transform = 'scale(1)'; }); let isDragging = false; let startX, startY, startLeft, startTop; ball.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = parseInt(ball.style.right) || 20; startTop = parseInt(ball.style.top) || 100; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = startX - e.clientX; const deltaY = e.clientY - startY; ball.style.right = Math.max(10, startLeft + deltaX) + 'px'; ball.style.top = Math.max(10, startTop + deltaY) + 'px'; }); document.addEventListener('mouseup', () => { isDragging = false; }); ball.addEventListener('click', (e) => { if (!isDragging) { toggleMainWindow(); } }); document.body.appendChild(ball); return ball; } function createMainWindow() { const window = document.createElement('div'); window.id = 'bili-monitor-window'; window.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 800px; height: 600px; background: white; border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); z-index: 1000000; display: none; overflow: hidden; `; const header = document.createElement('div'); header.style.cssText = ` background: linear-gradient(135deg, #00a1d6 0%, #f25d8e 100%); color: white; padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; cursor: move; `; header.innerHTML = `

B站评论弹幕记录

`; const tabNav = document.createElement('div'); tabNav.style.cssText = ` display: flex; background: #f8f9fa; border-bottom: 1px solid #e9ecef; `; const tabs = [ { id: 'comments', name: '评论监控', icon: '' }, { id: 'danmaku', name: '弹幕监控', icon: '' } ]; tabs.forEach((tab, index) => { const tabBtn = document.createElement('button'); tabBtn.className = 'tab-btn'; tabBtn.dataset.tab = tab.id; tabBtn.innerHTML = `${tab.icon} ${tab.name}`; tabBtn.style.cssText = ` flex: 1; padding: 12px; border: none; background: ${index === 0 ? 'white' : 'transparent'}; cursor: pointer; font-size: 14px; transition: all 0.3s ease; ${index === 0 ? 'border-bottom: 3px solid #00a1d6;' : ''} `; tabBtn.addEventListener('click', () => switchTab(tab.id)); tabNav.appendChild(tabBtn); }); const content = document.createElement('div'); content.id = 'tab-content'; content.style.cssText = ` flex: 1; overflow: hidden; padding: 20px; `; let isDragging = false; let startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = window.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; window.style.left = startLeft + deltaX + 'px'; window.style.top = startTop + deltaY + 'px'; window.style.transform = 'none'; }); document.addEventListener('mouseup', () => { isDragging = false; }); header.querySelector('#close-btn').addEventListener('click', () => { window.style.display = 'none'; }); header.querySelector('#export-btn').addEventListener('click', exportData); window.appendChild(header); window.appendChild(tabNav); window.appendChild(content); document.body.appendChild(window); return window; } function switchTab(tabId) { document.querySelectorAll('.tab-btn').forEach(btn => { const isActive = btn.dataset.tab === tabId; btn.style.background = isActive ? 'white' : 'transparent'; btn.style.borderBottom = isActive ? '3px solid #00a1d6' : 'none'; }); const content = document.getElementById('tab-content'); switch (tabId) { case 'comments': content.innerHTML = createCommentsTab(); updateCommentsDisplay(); break; case 'danmaku': content.innerHTML = createDanmakuTab(); updateDanmakuDisplay(); break; } } function createCommentsTab() { const imageComments = capturedComments.filter(c => c.images && c.images.length > 0); return `

评论监控 (${capturedComments.length}) ${imageComments.length > 0 ? `📷 含图片: ${imageComments.length}` : ''}

`; } function createDanmakuTab() { return `

弹幕监控 (${capturedDanmaku.length})

`; } function updateCommentsDisplay() { const listDiv = document.getElementById('comments-list'); if (!listDiv) return; if (capturedComments.length === 0) { listDiv.innerHTML = '
暂无评论数据
'; return; } listDiv.innerHTML = capturedComments.slice(-50).reverse().map(comment => { const hasImages = comment.images && comment.images.length > 0; return `
${escapeHtml(comment.text)}
${hasImages ? `
📷 包含图片 (${comment.images.length}张)
${comment.images.map((img, index) => `
图片 ${index + 1}: 查看原图 ${img.width ? `尺寸: ${img.width}×${img.height}px` : ''}
`).join('')}
` : ''}
⏰ ${comment.time} 🆔 ${comment.rpid || '未获取'} 📹 oid: ${comment.oid || '未获取'} ● ${comment.status} ${hasImages ? '📷 含图片' : ''}
`; }).join(''); const tab = document.querySelector('[data-tab="comments"]'); if (tab) { const imageCount = capturedComments.filter(c => c.images && c.images.length > 0).length; tab.innerHTML = `评论监控 (${capturedComments.length}${imageCount > 0 ? ` 📷${imageCount}` : ''})`; } } function updateDanmakuDisplay() { const listDiv = document.getElementById('danmaku-list'); if (!listDiv) return; if (capturedDanmaku.length === 0) { listDiv.innerHTML = '
暂无弹幕数据
'; return; } listDiv.innerHTML = capturedDanmaku.slice(-50).reverse().map(danmaku => `
${escapeHtml(danmaku.text)}
⏰ ${danmaku.time} 📺 ${danmaku.method || '发送'} ⏱️ ${danmaku.videoTime || 0}s 🎯 ${danmaku.pageType || '未知页面'}
`).join(''); const tab = document.querySelector('[data-tab="danmaku"]'); if (tab) { tab.innerHTML = `弹幕监控 (${capturedDanmaku.length})`; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function toggleMainWindow() { const window = document.getElementById('bili-monitor-window'); if (window.style.display === 'none') { window.style.display = 'flex'; window.style.flexDirection = 'column'; switchTab('comments'); } else { window.style.display = 'none'; } } function exportData() { let localStorageComments = []; let localStorageDanmaku = []; try { localStorageComments = JSON.parse(localStorage.getItem(CACHE_KEYS.comments) || '[]'); localStorageDanmaku = JSON.parse(localStorage.getItem(CACHE_KEYS.danmaku) || '[]'); console.log(`从localStorage导出 - 评论:${localStorageComments.length} 弹幕:${localStorageDanmaku.length}`); } catch (e) { console.error('localStorage读取失败,使用内存数据:', e); localStorageComments = capturedComments; localStorageDanmaku = capturedDanmaku; } const data = { exportTime: new Date().toISOString(), dataSource: "localStorage", summary: { totalComments: localStorageComments.length, totalDanmaku: localStorageDanmaku.length, imageComments: localStorageComments.filter(c => c.images && c.images.length > 0).length, totalImages: localStorageComments.reduce((sum, c) => sum + (c.images ? c.images.length : 0), 0) }, comments: localStorageComments.map(comment => ({ text: comment.text, time: comment.time, timestamp: comment.timestamp, rpid: comment.rpid, oid: comment.oid, type: comment.type, status: comment.status, images: comment.images || [], videoInfo: comment.videoInfo || getCurrentVideoInfo(), videoTime: comment.videoTime || getCurrentVideoTime(), pageType: comment.pageType || getPageType(), url: comment.url || window.location.href })), danmaku: localStorageDanmaku }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bili-monitor-localStorage-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log(`localStorage数据导出完成 - 评论:${data.summary.totalComments} 弹幕:${data.summary.totalDanmaku}`); } window.clearComments = () => { capturedComments = []; pendingComments.clear(); updateCommentsDisplay(); saveToCache(); console.log('评论数据已清空'); }; window.clearDanmaku = () => { capturedDanmaku = []; updateDanmakuDisplay(); saveToCache(); console.log('弹幕数据已清空'); }; function parseCommentData(data) { let requestData = {}; try { if (typeof data === 'string') { const params = new URLSearchParams(data); params.forEach((value, key) => { requestData[key] = value; }); } else if (data instanceof FormData) { data.forEach((value, key) => { requestData[key] = value; }); } } catch (e) { console.error('解析请求数据失败:', e); } return requestData; } function getCurrentVideoInfo() { try { const title = document.querySelector('.video-title, h1.title, .video-data__info h1')?.textContent?.trim() || '未知视频'; const bvid = window.location.href.match(/BV[\w]+/) ? window.location.href.match(/BV[\w]+/)[0] : '未知'; const upName = document.querySelector('.up-name, .up-info__right .up-name')?.textContent?.trim() || '未知UP主'; return { title, bvid, upName, url: window.location.href }; } catch (e) { return { title: '未知视频', bvid: '未知', upName: '未知UP主', url: window.location.href }; } } function getCurrentVideoTime() { try { const video = document.querySelector('video'); return video ? Math.floor(video.currentTime) : 0; } catch (e) { return 0; } } function getPageType() { const url = window.location.href; if (url.includes('/video/') || url.includes('/list/')) return '视频'; if (url.includes('/bangumi/')) return '番剧'; return '其他'; } class OptimizedNetworkInterceptor { constructor() { this.setupFetchInterceptor(); log('优化的网络拦截器已启动(仅Fetch)'); } setupFetchInterceptor() { const originalFetch = window.fetch; window.fetch = async function(...args) { const [url, options = {}] = args; if (typeof url !== 'string' || !options.body) { return originalFetch.apply(this, args); } const isDanmakuAPI = API_CONFIG.danmaku.some(api => url.includes(api)); const isCommentAPI = API_CONFIG.comment.some(api => url.includes(api)); if (!isDanmakuAPI && !isCommentAPI) { return originalFetch.apply(this, args); } const requestId = generateRequestId(url, options.body); if (!shouldProcessRequest(requestId)) { return originalFetch.apply(this, args); } if (isDanmakuAPI) { try { const params = new URLSearchParams(options.body); const msg = params.get('msg'); if (msg) { log('弹幕请求拦截', msg); setTimeout(() => { if (window.danmakuMonitor) { window.danmakuMonitor.recordDanmaku(msg, '网络请求'); } }, 100); } } catch (e) { console.error('弹幕请求解析失败:', e); } } if (isCommentAPI) { log('评论请求检测', url); const requestData = parseCommentData(options.body); const { images } = extractImageInfo(requestData); if (images.length > 0) { log('📷 请求中发现图片!', images.length); } const comment = { text: requestData.message || requestData.content || '(评论内容)', time: new Date().toLocaleTimeString(), timestamp: Date.now(), tempId: Date.now() + Math.random(), rpid: null, oid: requestData.oid || null, type: requestData.type || null, status: "等待ID", images: images, videoInfo: getCurrentVideoInfo(), videoTime: getCurrentVideoTime(), pageType: getPageType(), url: window.location.href }; capturedComments.push(comment); if (capturedComments.length > 2000) { capturedComments = capturedComments.slice(-2000); } pendingComments.set(comment.tempId, comment); updateCommentsDisplay(); saveToCache(); return originalFetch.apply(this, args).then(response => { const clonedResponse = response.clone(); clonedResponse.json().then(data => { if (data.code === 0 && data.data) { const rpid = data.data.rpid || data.data.rpid_str || (data.data.reply && data.data.reply.rpid); if (rpid) { log(`获取到评论ID: ${rpid}`); updatePendingComment(rpid, requestData.message || requestData.content || '', comment.tempId); } } }).catch(e => console.error('解析响应失败:', e)); return response; }); } return originalFetch.apply(this, args); }; } } class DanmakuMonitor { constructor() { this.videoInfo = {}; this.updateVideoInfo(); } start() { this.monitorInputAndButton(); log('弹幕监控已启动'); } updateVideoInfo() { this.videoInfo = getCurrentVideoInfo(); } monitorInputAndButton() { setInterval(() => { const inputSelectors = [ '.bpx-player-dm-input', '.bilibili-player-video-danmaku-input', 'input[placeholder*="发个友善的弹幕"]' ]; const buttonSelectors = [ '.bpx-player-dm-btn-send', '.bilibili-player-video-btn-send', 'button[class*="send"]' ]; const input = inputSelectors.map(s => document.querySelector(s)).find(Boolean); const button = buttonSelectors.map(s => document.querySelector(s)).find(Boolean); if (input && !input.hasAttribute('data-danmaku-monitored')) { input.setAttribute('data-danmaku-monitored', 'true'); input.addEventListener('keypress', (e) => { if (e.key === 'Enter' && input.value.trim()) { setTimeout(() => this.recordDanmaku(input.value.trim(), 'Enter键'), 100); } }); log('弹幕输入框监听已设置'); } if (button && !button.hasAttribute('data-danmaku-monitored')) { button.setAttribute('data-danmaku-monitored', 'true'); button.addEventListener('click', () => { if (input && input.value.trim()) { setTimeout(() => this.recordDanmaku(input.value.trim(), '点击按钮'), 100); } }); log('弹幕发送按钮监听已设置'); } }, 2000); } recordDanmaku(text, method = '未知') { if (!text || text.trim() === '') return; const danmaku = { text: text.trim(), time: new Date().toLocaleTimeString(), timestamp: Date.now(), method: method, videoInfo: this.videoInfo, videoTime: getCurrentVideoTime(), pageType: getPageType() }; const isDuplicate = capturedDanmaku.some(d => d.text === danmaku.text && Math.abs(d.timestamp - danmaku.timestamp) < 2000 ); if (!isDuplicate) { capturedDanmaku.unshift(danmaku); if (capturedDanmaku.length > 1000) { capturedDanmaku = capturedDanmaku.slice(0, 1000); } log('弹幕已记录', danmaku.text); updateDanmakuDisplay(); saveToCache(); } } } function monitorCommentInput() { setInterval(() => { const selectors = [ '.bili-rich-textarea__inner', '.brt-container', '.reply-box-textarea', 'textarea[placeholder*="评论"]', '[contenteditable="true"]' ]; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(element => { if (!element.hasAttribute('data-monitored')) { element.setAttribute('data-monitored', 'true'); element.addEventListener('keydown', function(e) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { setTimeout(() => handleCommentSend(element), 100); } }); } }); }); const sendSelectors = [ '.reply-box-send', '.brt-send-btn', 'button[class*="submit"]', 'button[class*="send"]', '.comment-submit button' ]; sendSelectors.forEach(selector => { const buttons = document.querySelectorAll(selector); buttons.forEach(btn => { if (!btn.hasAttribute('data-monitored')) { btn.setAttribute('data-monitored', 'true'); btn.addEventListener('click', () => { const parentForm = btn.closest('form, .reply-box, .comment-box, .brt-container'); const input = parentForm ? parentForm.querySelector('textarea, [contenteditable="true"]') : null; setTimeout(() => handleCommentSend(input), 100); }); } }); }); }, 1000); } function handleCommentSend(targetInput = null) { const inputSelectors = [ '.bili-rich-textarea__inner', '.brt-container', '.reply-box-textarea', 'textarea[placeholder*="评论"]', '[contenteditable="true"]' ]; let commentText = ''; if (targetInput) { const text = targetInput.value || targetInput.textContent || targetInput.innerText; if (text && text.trim()) { commentText = text.trim(); } } if (!commentText) { for (const selector of inputSelectors) { const inputs = document.querySelectorAll(selector); for (const input of inputs) { const text = input.value || input.textContent || input.innerText; if (text && text.trim()) { commentText = text.trim(); break; } } if (commentText) break; } } if (commentText) { const comment = { text: commentText, time: new Date().toLocaleTimeString(), timestamp: Date.now(), tempId: Date.now() + Math.random(), rpid: null, status: "等待ID", images: [], videoInfo: getCurrentVideoInfo(), videoTime: getCurrentVideoTime(), pageType: getPageType(), url: window.location.href }; capturedComments.push(comment); if (capturedComments.length > 2000) { capturedComments = capturedComments.slice(-2000); } pendingComments.set(comment.tempId, comment); log('捕获评论发送', commentText); updateCommentsDisplay(); saveToCache(); setTimeout(() => { if (pendingComments.has(comment.tempId) && !comment.rpid) { comment.status = "获取失败"; pendingComments.delete(comment.tempId); log(`评论ID获取超时: ${comment.tempId}`); updateCommentsDisplay(); saveToCache(); } }, 30000); } } function updatePendingComment(rpid, content, targetTempId = null) { log(`尝试匹配评论 rpid=${rpid}`); let matchedComment = null; const now = Date.now(); if (targetTempId && pendingComments.has(targetTempId)) { matchedComment = pendingComments.get(targetTempId); log('直接tempId匹配成功'); } if (!matchedComment && content && content.trim()) { for (const [tempId, comment] of pendingComments) { if (now - comment.timestamp < 30000) { const commentText = comment.text.trim(); const contentText = content.trim(); let actualContent = contentText; const replyMatch = contentText.match(/回复\s*@[^::]+\s*[::]\s*(.+)$/); if (replyMatch) { actualContent = replyMatch[1].trim(); } if (commentText === actualContent || commentText === contentText || commentText.includes(actualContent) || actualContent.includes(commentText)) { matchedComment = comment; log('内容匹配成功'); break; } } } } if (!matchedComment && pendingComments.size > 0) { for (const [tempId, comment] of pendingComments) { if (now - comment.timestamp < 30000) { if (!matchedComment || comment.timestamp > matchedComment.timestamp) { matchedComment = comment; } } } if (matchedComment) { log('使用时间匹配'); } } if (matchedComment) { matchedComment.rpid = rpid; matchedComment.status = "已获取ID"; const index = capturedComments.findIndex(c => c.tempId === matchedComment.tempId); if (index !== -1) { capturedComments[index] = { ...matchedComment }; } pendingComments.delete(matchedComment.tempId); log(`✅ 评论匹配成功: rpid=${rpid}`); updateCommentsDisplay(); saveToCache(); } else { log(`❌ 未找到匹配评论: rpid=${rpid}`); } } function init() { console.log('B站监控工具启动(优化版 v4.0)'); loadCachedData(); createFloatingBall(); createMainWindow(); new OptimizedNetworkInterceptor(); monitorCommentInput(); const danmakuMonitor = new DanmakuMonitor(); window.danmakuMonitor = danmakuMonitor; danmakuMonitor.start(); console.log('所有功能已启动'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(init, 1000)); } else { setTimeout(init, 1000); } })();