// ==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();
})();