// ==UserScript== // @name B站动态批量清理工具 // @namespace https://scriptcat.org // @version 3.1.0 // @description 一键批量删除B站个人空间所有动态。需授权密钥激活,闲鱼购买。 // @author 3390 // @match https://space.bilibili.com/*/dynamic // @icon https://www.bilibili.com/favicon.ico // @grant none // @run-at document-idle // @license Proprietary // @supportURL https://scriptcat.org/zh-CN/users/你的用户ID // @homepage https://scriptcat.org/zh-CN/script/你的脚本ID // ==/UserScript== (function () { 'use strict'; const LICENSE_SECRET = 'Xk9mP2qL7vR4wN8yF5jT1cD6hG0uE3aB'; const XPATH = { THREE_DOTS: '/html/body/div/main/div[1]/div[2]/div/div/div/div[1]/div[1]/div/div/div[2]/div[3]/div/div', DELETE_TEXT: '//div[contains(text(), "删除")]', CONFIRM_BTN: '/html/body/div[4]/div[2]/div[4]/button[2]', }; // ========================================== // 配置 // ========================================== const CONFIG = { speedPresets: { slow: { deleteDelay: 2000, scrollDelay: 3000 }, normal: { deleteDelay: 1000, scrollDelay: 2000 }, fast: { deleteDelay: 200, scrollDelay: 1000 }, }, maxFailAttempts: 3, debug: false, }; const STATE = { running: false, paused: false, deletedCount: 0, failCount: 0, currentSpeed: 'normal', licensed: false, licenseExpiry: null, }; // ========================================== // 许可证验证 // ========================================== const LICENSE_STORAGE_KEY = 'bili_cleaner_license_v3'; /** 尝试从 localStorage 恢复已保存的许可证 */ function loadSavedLicense() { try { const saved = localStorage.getItem(LICENSE_STORAGE_KEY); if (!saved) return null; const data = JSON.parse(saved); // 检查缓存的过期时间 if (data.expiryTs && Date.now() / 1000 < data.expiryTs) { return data.key; } // 过期了,清除 localStorage.removeItem(LICENSE_STORAGE_KEY); return null; } catch (e) { return null; } } /** 保存有效的许可证到 localStorage */ function saveLicense(key, expiryTs) { try { localStorage.setItem(LICENSE_STORAGE_KEY, JSON.stringify({ key: key, expiryTs: expiryTs, savedAt: Date.now(), })); } catch (e) { /* ignore */ } } /** 清除保存的许可证 */ function clearLicense() { try { localStorage.removeItem(LICENSE_STORAGE_KEY); } catch (e) { /* ignore */ } } /** * 验证 License Key(本地 HMAC-SHA256) * 发布前务必混淆代码,防止 SECRET 被提取 */ async function validateLicenseKey(key) { if (!key || typeof key !== 'string') { return { valid: false, reason: '请输入密钥' }; } key = key.trim(); if (!key.toUpperCase().startsWith('BILI-')) { return { valid: false, reason: '密钥格式错误,应以 BILI- 开头' }; } const encoded = key.slice(5); let decoded; try { decoded = atob(encoded); } catch (e) { return { valid: false, reason: '密钥格式无效' }; } const parts = decoded.split('|'); if (parts.length !== 2) { return { valid: false, reason: '密钥数据损坏' }; } const expiryTs = parseInt(parts[0], 10); if (isNaN(expiryTs) || expiryTs <= 0) { return { valid: false, reason: '密钥数据无效' }; } if (Date.now() / 1000 > expiryTs) { const d = new Date(expiryTs * 1000).toLocaleDateString('zh-CN'); return { valid: false, reason: '密钥已于 ' + d + ' 过期' }; } const expectedSig = parts[1]; const encoder = new TextEncoder(); const dataToHash = encoder.encode(parts[0] + LICENSE_SECRET); try { const hashBuffer = await crypto.subtle.digest('SHA-256', dataToHash); const hashHex = Array.from(new Uint8Array(hashBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join('') .toUpperCase(); if (hashHex.slice(0, 16) !== expectedSig) { return { valid: false, reason: '密钥无效,签名验证失败' }; } } catch (e) { return { valid: false, reason: '验证失败,请使用现代浏览器' }; } const expiryDate = new Date(expiryTs * 1000).toLocaleDateString('zh-CN'); return { valid: true, expiry: expiryDate, expiryTs: expiryTs }; } // ========================================== // 工具函数 // ========================================== const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const log = (...a) => { if (CONFIG.debug) console.log('[B站清理]', ...a); }; const randomSleep = (base, v = 0.3) => sleep(base + base * v * (Math.random() * 2 - 1)); function waitXPath(xpath, timeoutMs = 3000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const el = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (el) { const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) return el; } } catch (e) { /* ignore */ } } return null; } function triggerHover(el) { if (!el) return; const rect = el.getBoundingClientRect(); const opts = { view: window, bubbles: true, cancelable: true, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, }; el.dispatchEvent(new MouseEvent('mouseenter', opts)); el.dispatchEvent(new MouseEvent('mouseover', opts)); el.dispatchEvent(new PointerEvent('pointerenter', opts)); el.dispatchEvent(new PointerEvent('pointerover', opts)); el.focus(); } function triggerClick(el) { if (!el) return; el.click(); } function clickBlank() { document.body.click(); } async function scrollToLoad(delay) { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); await sleep(delay); window.scrollBy({ top: -300, behavior: 'smooth' }); await sleep(500); window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); await sleep(800); } // ========================================== // UI 面板 // ========================================== function createPanel() { const oldPanel = document.getElementById('bili-cleaner-panel'); if (oldPanel) oldPanel.remove(); const oldCollapse = document.getElementById('bili-cleaner-collapse'); if (oldCollapse) oldCollapse.remove(); const panel = document.createElement('div'); panel.id = 'bili-cleaner-panel'; const licensed = STATE.licensed; const expiryText = STATE.licenseExpiry ? '有效期至 ' + STATE.licenseExpiry : ''; panel.innerHTML = `
🧹 B站动态清理
${licensed ? '🔓 已激活 · ' + expiryText : '🔒 请输入授权密钥'}
${licensed ? '' : `
没有密钥?点击获取 →
`}
0 条已删除
${licensed ? '✅ 已授权,可以开始清理' : '⚠ 请输入授权密钥解锁功能'}
XPath 状态: 等待运行
`; document.body.appendChild(panel); const collapseBtn = document.createElement('button'); collapseBtn.id = 'bili-cleaner-collapse'; collapseBtn.className = 'bili-cleaner-collapse-btn'; collapseBtn.innerHTML = '🧹'; collapseBtn.title = '展开B站清理'; document.body.appendChild(collapseBtn); bindEvents(panel, collapseBtn); } // ========================================== // 事件绑定 // ========================================== function bindEvents(panel, collapseBtn) { const startBtn = document.getElementById('bili-cleaner-start'); const pauseBtn = document.getElementById('bili-cleaner-pause'); const stopBtn = document.getElementById('bili-cleaner-stop'); const statusEl = document.getElementById('bili-cleaner-status'); const countEl = document.getElementById('bili-cleaner-count'); const progressBar = document.getElementById('bili-cleaner-progress-bar'); const xpathDebugEl = document.getElementById('bili-cleaner-xpath-debug'); // 最小化 / 展开 document.getElementById('bili-cleaner-minimize').addEventListener('click', () => { panel.classList.add('collapsed'); collapseBtn.classList.add('show'); }); collapseBtn.addEventListener('click', () => { panel.classList.remove('collapsed'); collapseBtn.classList.remove('show'); }); // 拖拽 let dragging = false, startX, startY, startLeft, startTop; document.getElementById('bili-cleaner-drag-handle').addEventListener('mousedown', (e) => { dragging = true; startX = e.clientX; startY = e.clientY; const r = panel.getBoundingClientRect(); startLeft = r.left; startTop = r.top; panel.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!dragging) return; panel.style.right = 'auto'; panel.style.top = 'auto'; panel.style.left = (startLeft + e.clientX - startX) + 'px'; panel.style.top = (startTop + e.clientY - startY) + 'px'; }); document.addEventListener('mouseup', () => { if (dragging) { dragging = false; panel.style.transition = ''; } }); // 购买链接 const buyLink = document.getElementById('bili-cleaner-buy-link'); if (buyLink) { buyLink.href = 'https://www.goofish.com/item?spm=a21ybx.personal.feeds.5.64736ac2mYojwK&id=1012771373816&categoryId=50023914'; buyLink.target = '_blank'; } // 激活按钮 const activateBtn = document.getElementById('bili-cleaner-key-activate'); const keyInput = document.getElementById('bili-cleaner-key-input'); const hintEl = document.getElementById('bili-cleaner-license-hint'); const licenseBox = document.getElementById('bili-cleaner-license-box'); if (activateBtn && keyInput) { activateBtn.addEventListener('click', async () => { const rawKey = keyInput.value.trim(); if (!rawKey) { if (hintEl) hintEl.textContent = '请输入密钥'; return; } activateBtn.disabled = true; activateBtn.textContent = '验证中...'; if (hintEl) hintEl.textContent = '正在验证密钥...'; const result = await validateLicenseKey(rawKey); if (result.valid) { STATE.licensed = true; STATE.licenseExpiry = result.expiry; saveLicense(rawKey, result.expiryTs); // 刷新面板 createPanel(); } else { activateBtn.disabled = false; activateBtn.textContent = '激活'; if (hintEl) hintEl.textContent = '❌ ' + result.reason; if (hintEl) hintEl.style.color = '#e74c3c'; } }); // 回车激活 keyInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') activateBtn.click(); }); } // 速度切换 document.getElementById('bili-cleaner-speed-group').addEventListener('click', (e) => { const btn = e.target.closest('.bili-cleaner-speed-btn'); if (!btn) return; document.querySelectorAll('.bili-cleaner-speed-btn').forEach((b) => b.classList.remove('active')); btn.classList.add('active'); STATE.currentSpeed = btn.dataset.speed; }); // 开始 — 添加许可证门控 startBtn.addEventListener('click', async () => { if (!STATE.licensed) { updateStatus(statusEl, '⚠ 请先激活授权密钥', 'error'); return; } if (STATE.running) return; STATE.running = true; STATE.paused = false; STATE.deletedCount = 0; STATE.failCount = 0; countEl.textContent = '0'; progressBar.style.width = '0%'; startBtn.disabled = true; pauseBtn.disabled = false; stopBtn.disabled = false; startBtn.textContent = '⏳ 运行中...'; updateStatus(statusEl, '🚀 开始清理...', 'running'); setXpathDebug(xpathDebugEl, '就绪,等待匹配三点按钮...'); try { await deleteLoop(countEl, progressBar, statusEl, xpathDebugEl); } catch (e) { console.error('[B站清理]', e); updateStatus(statusEl, '❌ 异常: ' + e.message, 'error'); } finally { STATE.running = false; startBtn.disabled = false; pauseBtn.disabled = true; stopBtn.disabled = true; startBtn.textContent = '▶ 开始批量删除'; updateStatus(statusEl, '✅ 完成!共删除 ' + STATE.deletedCount + ' 条', ''); } }); // 暂停 / 继续 pauseBtn.addEventListener('click', () => { STATE.paused = !STATE.paused; pauseBtn.textContent = STATE.paused ? '▶ 继续' : '⏸ 暂停'; }); // 停止 stopBtn.addEventListener('click', () => { STATE.running = false; STATE.paused = false; }); } function updateStatus(el, msg, cls) { if (!el) return; el.textContent = msg; el.className = 'bili-cleaner-status ' + (cls || ''); } function setXpathDebug(el, msg) { if (!el) return; el.textContent = 'XPath 状态: ' + msg; } // ========================================== // 核心删除循环 // ========================================== async function deleteLoop(countEl, progressBar, statusEl, xpathDebugEl) { const cfg = CONFIG.speedPresets[STATE.currentSpeed]; let confirmStaleRetries = 0; while (STATE.running) { while (STATE.paused && STATE.running) await sleep(500); if (!STATE.running) break; // 步骤 0: 守护检查 const staleConfirm = waitXPath(XPATH.CONFIRM_BTN, 500); if (staleConfirm) { confirmStaleRetries++; log('⚠️ 检测到上一轮确认弹窗残留,尝试关闭 (' + confirmStaleRetries + ')'); setXpathDebug(xpathDebugEl, '⚠️ 残留确认弹窗 → 点击关闭'); staleConfirm.click(); await sleep(500); if (waitXPath(XPATH.CONFIRM_BTN, 500)) { log('弹窗未关闭,点击空白区域'); clickBlank(); await sleep(500); } if (confirmStaleRetries >= 3) { log('连续检测到残留弹窗,可能页面异常,重启流程'); setXpathDebug(xpathDebugEl, '⚠️ 残留弹窗3次 → 强制刷新页面状态'); confirmStaleRetries = 0; clickBlank(); await sleep(800); } continue; } confirmStaleRetries = 0; // 步骤 1: 找三点按钮 setXpathDebug(xpathDebugEl, '查找三点按钮...'); const menuBtn = waitXPath(XPATH.THREE_DOTS, 3000); if (!menuBtn) { STATE.failCount++; updateStatus(statusEl, '⏳ 暂无内容,滚动加载... (' + STATE.failCount + '/' + CONFIG.maxFailAttempts + ')', 'running'); setXpathDebug(xpathDebugEl, '❌ 三点按钮未找到 → 滚动 (' + STATE.failCount + '/' + CONFIG.maxFailAttempts + ')'); await scrollToLoad(cfg.scrollDelay); if (STATE.failCount >= CONFIG.maxFailAttempts) { updateStatus(statusEl, '🎉 所有动态已清理完毕!', ''); setXpathDebug(xpathDebugEl, '✅ 完成'); break; } continue; } STATE.failCount = 0; setXpathDebug(xpathDebugEl, '✅ 三点按钮已找到'); menuBtn.scrollIntoView({ block: 'center', behavior: 'smooth' }); await sleep(500); // 步骤 2: hover setXpathDebug(xpathDebugEl, 'hover 三点按钮,等待菜单...'); triggerHover(menuBtn); await sleep(600); if (!STATE.running) break; // 步骤 3: 点删除 setXpathDebug(xpathDebugEl, '查找"删除"选项...'); const deleteItem = waitXPath(XPATH.DELETE_TEXT, 3000); if (!deleteItem) { log('未找到"删除"选项,关闭菜单'); setXpathDebug(xpathDebugEl, '❌ "删除"未出现 → 关闭菜单重试'); clickBlank(); await sleep(500); continue; } setXpathDebug(xpathDebugEl, '✅ 点击"删除"'); triggerClick(deleteItem); await sleep(600); if (!STATE.running) break; // 步骤 4: 点确定 setXpathDebug(xpathDebugEl, '查找"确定"按钮...'); const confirmBtn = waitXPath(XPATH.CONFIRM_BTN, 3000); if (!confirmBtn) { log('未找到"确定"按钮'); setXpathDebug(xpathDebugEl, '❌ "确定"未出现 → 关闭弹窗重试'); clickBlank(); await sleep(500); continue; } setXpathDebug(xpathDebugEl, '✅ 点击"确定"'); triggerClick(confirmBtn); // 步骤 5: 等弹窗消失 setXpathDebug(xpathDebugEl, '等待弹窗关闭 & DOM 稳定...'); let waitTries = 0; while (waitXPath(XPATH.CONFIRM_BTN, 500) && waitTries < 10) { await sleep(500); waitTries++; } if (waitTries >= 10) { log('⚠️ 弹窗未在预期时间内关闭,强制点击空白'); clickBlank(); await sleep(1000); } await sleep(500); STATE.deletedCount++; countEl.textContent = STATE.deletedCount; updateStatus(statusEl, '✅ 已删除第 ' + STATE.deletedCount + ' 条动态', 'running'); progressBar.style.width = Math.min((STATE.deletedCount / (STATE.deletedCount + 5)) * 100, 95) + '%'; await randomSleep(cfg.deleteDelay); } } // ========================================== // 初始化 // ========================================== async function init() { console.log( '%c[B站清理] %c✅ 付费版脚本已加载 %c| ' + location.href, 'color:#fb7299;font-weight:bold', 'color:#67c23a', 'color:#999' ); // 尝试从 localStorage 恢复许可证 const savedKey = loadSavedLicense(); if (savedKey) { const result = await validateLicenseKey(savedKey); if (result.valid) { STATE.licensed = true; STATE.licenseExpiry = result.expiry; console.log('%c[B站清理] %c✅ 许可证已自动激活 · 有效期至 ' + result.expiry, 'color:#fb7299', 'color:#27ae60'); } else { clearLicense(); console.log('%c[B站清理] %c⚠️ 许可证已过期或无效,需要重新激活', 'color:#fb7299', 'color:#e67e22'); } } // 轮询等待 DOM 就绪 let tries = 0; const timer = setInterval(() => { tries++; if (document.body && location.href.includes('/dynamic')) { clearInterval(timer); createPanel(); } else if (tries > 30) { clearInterval(timer); if (document.body) createPanel(); } }, 1000); // 左下角标记 const showBadge = () => { if (!document.body || document.getElementById('bili-cleaner-badge')) return; const badge = document.createElement('div'); badge.id = 'bili-cleaner-badge'; badge.textContent = '🧹'; badge.title = 'B站动态清理工具已加载' + (STATE.licensed ? ' (已授权)' : ' (未激活)'); badge.style.cssText = 'position:fixed;bottom:20px;left:20px;z-index:2147483647;' + 'width:36px;height:36px;border-radius:50%;' + 'background:#fb7299;color:#fff;' + 'display:flex;align-items:center;justify-content:center;' + 'font-size:18px;box-shadow:0 2px 8px rgba(251,114,153,.5);' + 'cursor:pointer;opacity:.85;'; badge.onclick = () => { const p = document.getElementById('bili-cleaner-panel'); if (p) { p.classList.remove('collapsed'); const c = document.getElementById('bili-cleaner-collapse'); if (c) c.classList.remove('show'); badge.remove(); } }; document.body.appendChild(badge); }; if (document.body) { showBadge(); } else { const obs = new MutationObserver(() => { if (document.body) { obs.disconnect(); showBadge(); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); } } init(); })();