// ==UserScript== // @name B站批量拉黑工具 // @namespace https://github.com/Chuc-Jie/BilibiliBlockUserJs // @version 1.1.0 // @description 批量拉黑B站用户 // @author 友野YouyEr // @icon https://static.hdslb.com/images/favicon.ico // @match *://*.bilibili.com/* // @grant GM_addStyle // @grant GM_notification // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect bilibili.com // @connect api.bilibili.com // ==/UserScript== (function() { 'use strict'; // ==================== 样式注入(不变) ==================== GM_addStyle(` #batchBlockContainer { position: fixed; top: 80px; right: 20px; z-index: 9999; width: 450px; background: #fff; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); padding: 20px; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; border: 1px solid #fb7299; display: none; backdrop-filter: blur(2px); } #batchBlockContainer h3 { margin: 0 0 15px; color: #fb7299; font-size: 16px; text-align: center; } #usernameInput { width: 100%; height: 150px; margin-bottom: 10px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; font-family: monospace; font-size: 12px; box-sizing: border-box; } #controlBtns { display: flex; gap: 10px; margin-bottom: 15px; } .batch-btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background-color 0.2s; font-weight: bold; } #startBtn { background: #fb7299; color: white; flex: 1; } #startBtn:hover:not(:disabled) { background: #ff8ab0; } #stopBtn { background: #f44336; color: white; flex: 1; } #stopBtn:hover:not(:disabled) { background: #d32f2f; } #clearBtn { background: #eee; color: #333; } #clearBtn:hover { background: #ddd; } .batch-btn:disabled { background: #ccc; cursor: not-allowed; opacity: 0.7; } #logArea { height: 250px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 8px; font-size: 12px; line-height: 1.5; background: #f9f9f9; font-family: 'SF Mono', Monaco, monospace; } .success { color: #2e7d32; font-weight: bold; } .error { color: #c62828; font-weight: bold; } .info { color: #0d47a1; } .warning { color: #ef6c00; font-weight: bold; } .drag-handle { background: #fb7299; color: white; padding: 6px 12px; border-radius: 8px 8px 0 0; cursor: move; text-align: center; margin: -20px -20px 15px -20px; font-size: 13px; user-select: none; } .status-bar { font-size: 11px; color: #666; margin-bottom: 8px; display: flex; justify-content: space-between; } `); // ==================== 全局变量 ==================== let stopFlag = false; let isProcessing = false; // ==================== 辅助函数 ==================== function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); return null; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function randomDelay() { const min = 2000, max = 6000; const ms = Math.floor(Math.random() * (max - min + 1) + min); return delay(ms); } // 封装 GM_xmlhttpRequest 为 Promise function gmRequest(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, onload: function(response) { // 状态码 2xx 视为成功 if (response.status >= 200 && response.status < 300) { resolve(response); } else { reject(new Error(`HTTP ${response.status}: ${response.responseText.slice(0, 100)}`)); } }, onerror: function(err) { reject(new Error('网络请求失败 (GM_xmlhttpRequest 错误)')); }, ontimeout: function() { reject(new Error('请求超时')); }, timeout: 15000 }); }); } // 带重试的请求包装器(使用 GM_xmlhttpRequest) async function requestWithRetry(url, options, retries = 3, baseDelay = 2000) { for (let i = 0; i < retries; i++) { try { const response = await gmRequest({ url, ...options }); // 检查响应状态码(已在 onload 中处理 2xx,但这里再判断一次) if (response.status === 412 || response.status === 429) { throw new Error(`HTTP ${response.status} - 触发风控`); } return response; } catch (err) { const isLast = i === retries - 1; if (isLast) throw err; const waitTime = baseDelay * Math.pow(2, i); log(`请求失败 (${err.message}),${waitTime/1000}秒后重试 (${i+1}/${retries})`, 'warning'); await delay(waitTime); } } } // 构建请求头(模拟浏览器,但简化 Accept-Encoding) function getRequestHeaders(extra = {}) { const base = { 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'User-Agent': navigator.userAgent, 'Referer': 'https://www.bilibili.com/', 'Origin': 'https://www.bilibili.com', 'DNT': '1', 'Sec-GPC': '1' }; // 添加现代浏览器的 sec-ch-ua 头(可选,不影响主要功能) if (navigator.userAgentData && navigator.userAgentData.brands) { base['sec-ch-ua'] = navigator.userAgentData.brands.map(b => `"${b.brand}";v="${b.version}"`).join(', '); base['sec-ch-ua-mobile'] = navigator.userAgentData.mobile ? '?1' : '?0'; base['sec-ch-ua-platform'] = `"${navigator.userAgentData.platform}"`; } return { ...base, ...extra }; } // 预热会话(GM_xmlhttpRequest) async function warmupSession() { try { await gmRequest({ url: 'https://www.bilibili.com/', method: 'GET', headers: getRequestHeaders(), credentials: 'include' }); log('会话预热完成', 'info'); } catch (e) { log('预热请求失败,不影响主功能', 'warning'); } } // ==================== 核心功能 ==================== async function resolveUserIdentifier(input) { input = input.trim(); // 情况1:纯数字 => UID if (/^\d+$/.test(input)) { log(`检测到UID输入: ${input},正在验证...`, 'info'); const url = `https://api.bilibili.com/x/space/acc/info?mid=${input}`; const response = await requestWithRetry(url, { method: 'GET', headers: getRequestHeaders({ 'Referer': 'https://space.bilibili.com/' }), credentials: 'include' }); const data = JSON.parse(response.responseText); if (data.code === 0 && data.data && data.data.mid) { const username = data.data.name; log(`UID验证成功: ${username} (${input})`, 'info'); return { uid: input, name: username }; } else { throw new Error(`UID无效或不存在: ${data.message || '未知错误'}`); } } // 情况2:用户名搜索 log(`检测到用户名输入: ${input},正在搜索...`, 'info'); const searchUrl = `https://api.bilibili.com/x/web-interface/search/type?search_type=bili_user&keyword=${encodeURIComponent(input)}&order=totalrank`; const response = await requestWithRetry(searchUrl, { method: 'GET', headers: getRequestHeaders({ 'Referer': 'https://search.bilibili.com/' }), credentials: 'include' }); const data = JSON.parse(response.responseText); if (data.code !== 0) { throw new Error(`搜索接口错误: ${data.message || data.code}`); } if (!data.data || !data.data.result || data.data.result.length === 0) { throw new Error('未找到该用户'); } // 精确匹配 let exactUser = data.data.result.find(item => item.uname && item.uname.trim() === input.trim() ); if (exactUser && exactUser.mid) { return { uid: exactUser.mid, name: exactUser.uname }; } // 模糊匹配 const firstUser = data.data.result[0]; if (firstUser && firstUser.mid) { log(`未精确匹配到“${input}”,将使用相似用户“${firstUser.uname}”(UID:${firstUser.mid})`, 'warning'); return { uid: firstUser.mid, name: firstUser.uname }; } throw new Error('未找到任何匹配用户'); } async function blockUser(uid, username) { const biliJct = getCookie('bili_jct'); if (!biliJct) { throw new Error('未找到CSRF令牌(bili_jct),请重新登录B站'); } const body = new URLSearchParams({ fid: uid, act: 5, re_src: 11, csrf: biliJct }); const response = await requestWithRetry('https://api.bilibili.com/x/relation/modify', { method: 'POST', headers: getRequestHeaders({ 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://space.bilibili.com/' }), data: body.toString(), credentials: 'include' }); const data = JSON.parse(response.responseText); if (data.code !== 0) { throw new Error(`拉黑失败: ${data.message || '未知错误'}`); } log(`✅ 成功拉黑: ${username || uid} (UID: ${uid})`, 'success'); return true; } // ==================== UI 日志与状态 ==================== function log(message, type = 'info') { const logArea = document.getElementById('logArea'); if (!logArea) return; const logItem = document.createElement('div'); logItem.className = type; logItem.innerHTML = `[${new Date().toLocaleTimeString()}] ${message}`; logArea.appendChild(logItem); logArea.scrollTop = logArea.scrollHeight; console.log(`[${type}] ${message}`); } function updateStatus(status, progress) { const statusEl = document.getElementById('statusText'); const progressEl = document.getElementById('progressText'); if (statusEl) statusEl.textContent = status; if (progressEl) progressEl.textContent = progress; } function parseUsernames(input) { input = input.trim(); if (!input) return []; if (input.startsWith('[') && input.endsWith(']')) { try { const jsonStr = input.replace(/'/g, '"'); const list = JSON.parse(jsonStr); if (Array.isArray(list) && list.every(item => typeof item === 'string')) { return list.filter(name => name.trim()); } } catch (e) { log(`JSON解析失败,按行解析: ${e.message}`, 'warning'); } } return input.split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } // ==================== 批量拉黑主流程 ==================== async function startBatchBlock() { if (isProcessing) { log('已有任务正在运行,请勿重复点击', 'warning'); return; } const input = document.getElementById('usernameInput').value; const userInputs = parseUsernames(input); if (userInputs.length === 0) { log('请输入有效的用户名或UID(每行一个或JSON数组)', 'error'); GM_notification({ text: '请输入有效的用户名或UID', title: '批量拉黑', timeout: 3000 }); return; } stopFlag = false; isProcessing = true; const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); startBtn.disabled = true; stopBtn.disabled = false; startBtn.textContent = '处理中...'; let successCount = 0, failCount = 0; const total = userInputs.length; updateStatus('运行中', `0/${total}`); for (let i = 0; i < userInputs.length; i++) { if (stopFlag) { log('⚠️ 用户已手动停止批量任务', 'warning'); break; } const identifier = userInputs[i]; const current = i + 1; updateStatus('处理中', `${current}/${total}`); log(`[${current}/${total}] 正在处理: ${identifier}`, 'info'); try { const { uid, name } = await resolveUserIdentifier(identifier); await blockUser(uid, name); successCount++; } catch (err) { log(`❌ 失败: ${identifier} → ${err.message}`, 'error'); failCount++; } if (!stopFlag && current < total) { await randomDelay(); if (current % 20 === 0) { log('批量处理已连续20个,休息10秒避免风控...', 'info'); await delay(10000); } } } isProcessing = false; startBtn.disabled = false; stopBtn.disabled = true; startBtn.textContent = '开始批量拉黑'; const finalMsg = `批量完成 | 成功: ${successCount} | 失败: ${failCount} | 总计: ${total}`; log(finalMsg, successCount === total ? 'success' : 'warning'); updateStatus(stopFlag ? '已停止' : '完成', `${successCount}/${total}`); GM_notification({ text: `批量拉黑结束:成功 ${successCount} 个,失败 ${failCount} 个`, title: 'B站批量拉黑工具', timeout: 5000 }); } function stopBatch() { if (isProcessing) { stopFlag = true; log('正在停止任务,请稍候...', 'warning'); } else { log('当前没有正在运行的任务', 'info'); } } function clearAll() { if (isProcessing) { log('请先停止当前任务再清空', 'warning'); return; } document.getElementById('usernameInput').value = ''; document.getElementById('logArea').innerHTML = ''; updateStatus('就绪', '0/0'); log('已清空所有内容', 'info'); } // ==================== 面板拖拽 ==================== function makeDraggable(element, handle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; handle.onmousedown = dragMouseDown; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; let top = element.offsetTop - pos2; let left = element.offsetLeft - pos1; top = Math.min(window.innerHeight - 50, Math.max(0, top)); left = Math.min(window.innerWidth - element.offsetWidth, Math.max(0, left)); element.style.top = top + "px"; element.style.left = left + "px"; element.style.bottom = 'auto'; element.style.right = 'auto'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } function createPanel() { if (document.getElementById('batchBlockContainer')) { return document.getElementById('batchBlockContainer'); } const container = document.createElement('div'); container.id = 'batchBlockContainer'; container.innerHTML = `
⇳ 拖拽移动

B站批量拉黑工具 v1.4

状态: 就绪 进度: 0/0
🚀 使用GM_xmlhttpRequest | 稳定跨域 | 支持UID
`; document.body.appendChild(container); document.getElementById('startBtn').addEventListener('click', startBatchBlock); document.getElementById('stopBtn').addEventListener('click', stopBatch); document.getElementById('clearBtn').addEventListener('click', clearAll); makeDraggable(container, container.querySelector('.drag-handle')); return container; } function showPanel() { const panel = createPanel(); panel.style.display = 'block'; log('面板已显示,支持直接输入UID或用户名', 'info'); } function hidePanel() { const panel = document.getElementById('batchBlockContainer'); if (panel) panel.style.display = 'none'; } // ==================== 初始化 ==================== async function init() { warmupSession().catch(e => console.warn); GM_registerMenuCommand('📌 显示批量拉黑面板', showPanel); GM_registerMenuCommand('🙈 隐藏批量拉黑面板', hidePanel); if (!getCookie('bili_jct')) { log('⚠️ 未检测到 bili_jct Cookie,请确保已登录B站账号', 'warning'); } else { log('脚本已就绪,可通过脚本猫菜单打开面板', 'info'); } } init(); })();