// ==UserScript==
// @name B站动态批量删除小工具 - 交互版
// @version 2.1
// @description B站动态批量删除工具,支持在删除抽奖动态时预览内容并逐条确认
// @author Gemini
// @match https://space.bilibili.com/*
// @match http://space.bilibili.com/*
// @icon https://static.hdslb.com/images/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ================= 配置区域 =================
const CONFIG = {
// 请求间隔 (毫秒),太快会被B站拦截
INTERVAL: 800,
// 如果失败,重试次数
RETRY_LIMIT: 3,
AUTO_RELOAD: false // 改为 false 就不会自动刷新了
};
// 动态类型映射
const DYNAMIC_TYPES = {
REPOST: 1,
IMAGE: 2,
TEXT: 4,
VIDEO: 8,
SHORT_VIDEO: 16,
ARTICLE: 64
};
// 获取UID和CSRF Token
const uid = window.location.pathname.split("/")[1];
function getCSRFToken() {
const match = document.cookie.match(/bili_jct=([^;]+)/);
return match ? match[1] : '';
}
const csrfToken = getCSRFToken();
// ================= 工具类:解析动态内容 =================
class ContentParser {
static parse(cardStr, desc) {
try {
const card = JSON.parse(cardStr);
let content = "无法解析内容";
let originContent = "";
// 1. 提取自己的评论/文字
if (card.item && card.item.content) {
content = card.item.content;
} else if (card.item && card.item.description) {
content = card.item.description;
} else if (desc && desc.dynamic_id_str) {
content = "(无文字内容)";
}
// 2. 如果是转发,提取原动态信息
if (card.origin) {
try {
const origin = JSON.parse(card.origin);
let originText = "";
if (origin.item && (origin.item.content || origin.item.description)) {
originText = origin.item.content || origin.item.description;
} else if (origin.title) {
originText = origin.title;
} else if (origin.desc) {
originText = origin.desc;
}
const author = origin.user ? origin.user.name : (origin.owner ? origin.owner.name : "未知用户");
originContent = `[原动态 @${author}]: ${originText}`;
} catch (e) {
originContent = "[原动态已失效或无法解析]";
}
}
return {
text: content,
origin: originContent,
isLottery: this.checkIsLottery(card)
};
} catch (e) {
console.error("解析动态失败", e);
return { text: "解析数据格式错误", origin: "", isLottery: false };
}
}
static checkIsLottery(card) {
// 检查标准抽奖字段
if (card.origin_extension && card.origin_extension.lott) return true;
// 检查扩展字段
if (card.origin_extend_json) {
try {
const ext = JSON.parse(card.origin_extend_json);
if (ext.lott) return true;
} catch (e) {}
}
return false;
}
}
// ================= API类 =================
class DynamicAPI {
constructor() {
this.baseUrl = 'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr';
}
async getSpaceHistory(offset = 0) {
const url = `${this.baseUrl}/space_history?visitor_uid=${uid}&host_uid=${uid}&offset_dynamic_id=${offset}`;
try {
const response = await fetch(url, { credentials: 'include' });
return await response.json();
} catch (error) {
console.error('获取动态失败:', error);
throw error;
}
}
async deleteDynamic(dynamicId) {
const url = `${this.baseUrl}/rm_dynamic`;
const formData = new URLSearchParams();
formData.append('dynamic_id', dynamicId);
formData.append('csrf_token', csrfToken);
try {
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData
});
return await response.json();
} catch (error) {
console.error('删除动态失败:', error);
throw error;
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
const api = new DynamicAPI();
let isProcessing = false;
let deleteCount = 0;
// ================= UI管理器 =================
class UIManager {
constructor() {
this.panel = null;
this.resultBox = null;
this.modal = null;
}
createPanel() {
const panel = document.createElement('div');
panel.className = 'bili-cleaner-panel';
panel.innerHTML = `
`;
this.addStyles();
return panel;
}
addStyles() {
const style = document.createElement('style');
style.textContent = `
.bili-cleaner-panel { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); font-family: sans-serif; }
.cleaner-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; }
.cleaner-header h3 { margin: 0; color: #00a1d6; font-size: 16px; }
.status-indicator { font-size: 12px; color: #666; background: #f4f4f4; padding: 2px 8px; border-radius: 4px; }
.status-indicator.running { color: #fff; background: #f6a500; }
.action-buttons { display: flex; gap: 10px; margin-bottom: 15px; }
.cleaner-btn { flex: 1; padding: 8px 0; border: 1px solid #e7e7e7; background: #f9f9f9; border-radius: 6px; cursor: pointer; transition: all 0.2s; font-size: 13px; color: #333; }
.cleaner-btn:hover { border-color: #00a1d6; color: #00a1d6; background: #eef8fc; }
.cleaner-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.result-box { background: #f4f5f7; border-radius: 6px; padding: 10px; font-size: 12px; color: #555; max-height: 100px; overflow-y: auto; }
/* 弹窗样式 */
.cleaner-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 10000; display: flex; justify-content: center; align-items: center; }
.cleaner-modal { background: #fff; width: 400px; max-width: 90%; border-radius: 12px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: popIn 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
.modal-title { font-size: 16px; font-weight: bold; margin-bottom: 15px; color: #333; display: flex; justify-content: space-between; }
.modal-content { background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #eee; margin-bottom: 20px; font-size: 14px; line-height: 1.5; max-height: 300px; overflow-y: auto; }
.modal-origin { margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ccc; color: #888; font-size: 13px; }
.modal-actions { display: flex; gap: 10px; }
.modal-btn { flex: 1; padding: 10px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; }
.btn-confirm-del { background: #ff4d4d; color: white; }
.btn-skip { background: #e7e7e7; color: #333; }
.btn-stop { background: #333; color: white; }
.btn-confirm-del:hover { background: #ff3333; }
.btn-skip:hover { background: #d7d7d7; }
@keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
`;
document.head.appendChild(style);
}
updateStatus(text, isRunning = false) {
const el = document.getElementById('statusIndicator');
if (el) {
el.textContent = text;
el.className = `status-indicator ${isRunning ? 'running' : ''}`;
}
}
log(msg, isError = false) {
const box = document.querySelector('.result-content');
if (box) {
const time = new Date().toTimeString().split(' ')[0];
const p = document.createElement('div');
p.style.color = isError ? '#ff4d4d' : '#333';
p.textContent = `[${time}] ${msg}`;
box.insertBefore(p, box.firstChild);
}
}
// 显示交互弹窗 (返回 Promise,等待用户点击)
async showConfirmModal(parsedData) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'cleaner-modal-overlay';
overlay.innerHTML = `
🎁 发现抽奖动态
${parsedData.text}
${parsedData.origin ? `
${parsedData.origin}
` : ''}
`;
document.body.appendChild(overlay);
const cleanup = () => { document.body.removeChild(overlay); };
document.getElementById('c_del').onclick = () => { cleanup(); resolve('delete'); };
document.getElementById('c_skip').onclick = () => { cleanup(); resolve('skip'); };
document.getElementById('c_stop').onclick = () => { cleanup(); resolve('stop'); };
});
}
}
// ================= 核心逻辑 =================
const uiManager = new UIManager();
// 通用删除处理器
async function processDynamics(mode) {
if (isProcessing) return;
isProcessing = true;
deleteCount = 0;
uiManager.updateStatus('运行中...', true);
uiManager.log(`开始任务: ${mode === 'lottery' ? '筛选抽奖' : (mode === 'repost' ? '删除转发' : '全部删除')}`);
// 禁用按钮
document.querySelectorAll('.cleaner-btn').forEach(b => b.disabled = true);
let offset = 0;
let hasMore = true;
let consecutiveErrors = 0;
try {
while (hasMore && isProcessing) {
// 1. 获取历史记录
let data;
try {
const res = await api.getSpaceHistory(offset);
if (res.code !== 0) throw new Error(res.message);
data = res.data;
consecutiveErrors = 0; // 重置错误计数
} catch (e) {
consecutiveErrors++;
uiManager.log(`获取列表失败 (${consecutiveErrors}/${CONFIG.RETRY_LIMIT}): ${e.message}`, true);
if (consecutiveErrors >= CONFIG.RETRY_LIMIT) break;
await api.sleep(2000);
continue;
}
if (!data || !data.cards || data.cards.length === 0) break;
hasMore = data.has_more;
// 2. 遍历当前页的卡片
for (const card of data.cards) {
if (!isProcessing) break;
// 更新offset为当前card的id,用于下次翻页(非常重要,否则会死循环)
offset = card.desc.dynamic_id_str;
const dynamicId = card.desc.dynamic_id_str;
const cardStr = card.card;
const type = card.desc.type;
const origId = card.desc.orig_dy_id;
let shouldDelete = false;
let parsedContent = null;
// 解析内容
parsedContent = ContentParser.parse(cardStr, card.desc);
// --- 模式判断 ---
if (mode === 'all') {
shouldDelete = true;
}
else if (mode === 'repost') {
// 转发类型为1,或者 orig_dy_id 不为0
if (type === DYNAMIC_TYPES.REPOST || origId !== 0) shouldDelete = true;
}
else if (mode === 'lottery') {
// 如果是抽奖,需要弹窗确认
if (parsedContent.isLottery) {
// 暂停并等待用户交互
const action = await uiManager.showConfirmModal(parsedContent);
if (action === 'stop') {
isProcessing = false;
uiManager.log('用户终止操作');
break;
}
if (action === 'delete') {
shouldDelete = true;
} else {
uiManager.log(`已跳过: ${parsedContent.text.substring(0, 20)}...`);
}
}
}
// --- 执行删除 ---
if (shouldDelete) {
try {
const delRes = await api.deleteDynamic(dynamicId);
if (delRes.code === 0) {
deleteCount++;
uiManager.log(`已删除[${deleteCount}]: ${parsedContent.text.substring(0, 15)}...`);
} else {
uiManager.log(`删除失败: ${delRes.message}`, true);
}
// 成功删除后稍微等待,避免频繁请求
await api.sleep(CONFIG.INTERVAL);
} catch (e) {
uiManager.log(`API请求错误: ${e.message}`, true);
}
}
}
// 翻页间隙
await api.sleep(1000);
}
} catch (e) {
console.error(e);
uiManager.log(`发生严重错误: ${e.message}`, true);
} finally {
isProcessing = false;
uiManager.updateStatus('完成');
uiManager.log(`任务结束,共删除 ${deleteCount} 条动态`);
document.querySelectorAll('.cleaner-btn').forEach(b => b.disabled = false);
// 3秒后刷新
if (CONFIG.AUTO_RELOAD) {
setTimeout(() => location.reload(), 3000); }
}
}
// ================= 初始化 =================
async function init() {
// 延时确保页面加载
await new Promise(r => setTimeout(r, 1500));
const panel = uiManager.createPanel();
// 尝试插入位置:B站新旧版界面不同
const containers = [
"#app > main > div.space-dynamic > div.space-dynamic__right", // 新版
"#page-dynamic .col-2", // 旧版
".bili-dyn-home--right" // 另一种新版
];
let inserted = false;
for (const sel of containers) {
const c = document.querySelector(sel);
if (c) {
if (c.firstChild) c.insertBefore(panel, c.firstChild);
else c.appendChild(panel);
inserted = true;
break;
}
}
if (!inserted) {
// 如果都找不到,就悬浮显示
panel.style.position = 'fixed';
panel.style.bottom = '20px';
panel.style.right = '20px';
panel.style.width = '300px';
panel.style.zIndex = '9999';
document.body.appendChild(panel);
}
// 绑定事件
document.getElementById('btnDeleteAll').onclick = () => {
if(confirm('警告:确定要删除【所有】动态吗?不可恢复!')) processDynamics('all');
};
document.getElementById('btnDeleteLottery').onclick = () => processDynamics('lottery');
document.getElementById('btnDeleteRepost').onclick = () => {
if(confirm('确定要删除所有【转发】动态吗?')) processDynamics('repost');
};
console.log('B站动态清理助手交互版已加载');
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();