// ==UserScript==
// @name B站动态批量删除小工具 - 交互版
// @namespace https://scriptcat.org/zh-CN/users/210832
// @version 2.15
// @description B站动态批量删除工具,适配新版 Polymer 接口,支持全部删除/抽奖筛选/转发清理/关键词匹配删除
// @author GLM5.2
// @tags B站,工具,清理动态
// @match https://space.bilibili.com/*
// @match http://space.bilibili.com/*
// @icon https://static.hdslb.com/images/favicon.ico
// @grant none
// @license GPL-3.0-or-later
// @supportURL https://scriptcat.org/zh-CN/users/210832
// @compatible chrome
// @compatible edge
// @compatible firefox
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ================= 配置区域 =================
const CONFIG = {
// 请求间隔 (毫秒),太快会被B站拦截
INTERVAL: 800,
// 如果失败,重试次数
RETRY_LIMIT: 3,
// 删除完成后是否自动刷新页面
AUTO_RELOAD: false
};
// [修复] 新版动态类型用字符串常量表示
const DYNAMIC_TYPES = {
FORWARD: 'DYNAMIC_TYPE_FORWARD',
DRAW: 'DYNAMIC_TYPE_DRAW',
WORD: 'DYNAMIC_TYPE_WORD',
AV: 'DYNAMIC_TYPE_AV',
SHORT: 'DYNAMIC_TYPE_SHORT',
ARTICLE: 'DYNAMIC_TYPE_ARTICLE'
};
// 获取UID和CSRF Token
const uid = window.location.pathname.split("/")[1];
function getCSRFToken() {
const m = document.cookie.match(/bili_jct=([^;]+)/);
return m ? m[1] : '';
}
const csrfToken = getCSRFToken();
// [修复] 公共请求头,防止被B站风控拦截返回HTML
const COMMON_HEADERS = {
'Accept': 'application/json, text/plain, */*',
'Referer': 'https://www.bilibili.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'
};
// ================= 工具类:解析动态内容 =================
class ContentParser {
// [修复] 适配新版 modules 结构
static parse(item) {
let text = '(无文字内容)';
let originText = '';
const dyn = item.modules && item.modules.module_dynamic;
if (dyn) {
// 提取正文
if (dyn.desc && dyn.desc.text) {
text = dyn.desc.text;
}
// 如果没有正文,尝试从 major (视频/文章/图片) 提取标题
if ((!text || text === '(无文字内容)') && dyn.major) {
const m = dyn.major;
if (m.opus && m.opus.summary && m.opus.summary.text) text = m.opus.summary.text;
else if (m.archive && m.archive.title) text = m.archive.title;
else if (m.article && m.article.title) text = m.article.title;
}
}
// 提取转发动态的原动态信息
let origin = item.orig || (dyn && dyn.major && dyn.major.opus && dyn.major.opus.orig);
if (origin) {
try {
let oText = '';
if (origin.modules && origin.modules.module_dynamic) {
const od = origin.modules.module_dynamic;
oText = (od.desc && od.desc.text) ||
(od.major && od.major.opus && od.major.opus.summary && od.major.opus.summary.text) ||
(od.major && od.major.archive && od.major.archive.title) || '';
} else if (origin.item) {
oText = origin.item.content || origin.item.description || '';
}
const author = (origin.modules && origin.modules.module_author && origin.modules.module_author.name)
|| (origin.user && origin.user.name)
|| '未知用户';
originText = `[原动态 @${author}]: ${oText || '(无内容)'}`;
} catch (e) {
originText = '[原动态已失效或无法解析]';
}
}
return {
text,
origin: originText,
isLottery: this.checkIsLottery(item)
};
}
// [修复] 抽奖判断:兼容多种字段位置 + 关键词兜底
static checkIsLottery(item) {
const orig = item.orig;
if (orig) {
if (orig.origin_extension && orig.origin_extension.lott) return true;
if (orig.origin_extend_json) {
try { if (JSON.parse(orig.origin_extend_json).lott) return true; } catch (e) {}
}
if (orig.modules && orig.modules.module_dynamic && orig.modules.module_dynamic.additional) {
const add = orig.modules.module_dynamic.additional;
if (add.lottery || (add.common && JSON.stringify(add.common).includes('lottery'))) return true;
}
}
const dyn = item.modules && item.modules.module_dynamic;
if (dyn && dyn.additional && dyn.additional.lottery) return true;
try {
if (JSON.stringify(item).toLowerCase().includes('"lottery"')) return true;
} catch (e) {}
return false;
}
}
// ================= API类(适配新版端点) =================
class DynamicAPI {
constructor() {
// [修复] 列表和删除接口全部替换为 api.bilibili.com 的新端点
this.listUrl = 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space';
this.delUrl = 'https://api.bilibili.com/x/dynamic/feed/operate/remove';
}
async getSpaceHistory(offset = '') {
// [修复] 参数改为 host_mid + offset(首次为空字符串)
const url = `${this.listUrl}?host_mid=${uid}&offset=${encodeURIComponent(offset)}&timezone_offset=-480&platform=web`;
const res = await fetch(url, {
method: 'GET',
credentials: 'include',
headers: COMMON_HEADERS
});
// [修复] 先拿文本,避免直接 .json() 抛错无法定位
const txt = await res.text();
if (!txt || txt.trim().startsWith('<')) {
throw new Error(`返回非JSON(HTTP ${res.status}):${txt.slice(0, 60)}`);
}
return JSON.parse(txt);
}
async deleteDynamic(dynamicId) {
const url = `${this.delUrl}?csrf=${csrfToken}`;
// [修复] 新版删除用 JSON body,字段名 dyn_id_str
const res = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
...COMMON_HEADERS,
'Content-Type': 'application/json'
},
body: JSON.stringify({ dyn_id_str: String(dynamicId) })
});
const txt = await res.text();
if (!txt || txt.trim().startsWith('<')) {
throw new Error(`删除返回非JSON(HTTP ${res.status})`);
}
return JSON.parse(txt);
}
sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
}
const api = new DynamicAPI();
let isProcessing = false;
let deleteCount = 0;
// ================= UI管理器 =================
class UIManager {
constructor() { this.panel = 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,.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; }
/* [新增] flex-wrap 允许按钮换行,避免挤压 */
.action-buttons { display:flex; flex-wrap: wrap; gap:10px; margin-bottom:15px; }
.cleaner-btn { flex: 1 1 45%; padding:8px 0; border:1px solid #e7e7e7; background:#f9f9f9; border-radius:6px; cursor:pointer; font-size:13px; color:#333; transition:all .2s; }
.cleaner-btn:hover { border-color:#00a1d6; color:#00a1d6; background:#eef8fc; }
.cleaner-btn:disabled { opacity:.6; cursor:not-allowed; }
.result-box { background:#f4f5f7; border-radius:6px; padding:10px; font-size:12px; color:#555; max-height:120px; overflow-y:auto; }
.cleaner-modal-overlay { position:fixed; inset:0; background:rgba(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,.2); }
.modal-title { font-size:16px; font-weight:bold; margin-bottom:15px; color:#333; }
.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:#fff; } .btn-skip{background:#e7e7e7;color:#333;} .btn-stop{background:#333;color:#fff;}
`;
document.head.appendChild(style);
}
updateStatus(text, running = false) {
const el = document.getElementById('statusIndicator');
if (el) { el.textContent = text; el.className = `status-indicator ${running ? 'running' : ''}`; }
}
log(msg, isError = false) {
const box = document.querySelector('.result-content');
if (box) {
const t = new Date().toTimeString().split(' ')[0];
const p = document.createElement('div');
p.style.color = isError ? '#ff4d4d' : '#333';
p.textContent = `[${t}] ${msg}`;
box.insertBefore(p, box.firstChild);
}
}
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 rm = () => document.body.removeChild(overlay);
document.getElementById('c_del').onclick = () => { rm(); resolve('delete'); };
document.getElementById('c_skip').onclick = () => { rm(); resolve('skip'); };
document.getElementById('c_stop').onclick = () => { rm(); resolve('stop'); };
});
}
}
// ================= 核心逻辑 =================
const uiManager = new UIManager();
// [新增] 接收 keyword 参数
async function processDynamics(mode, keyword = '') {
if (isProcessing) return;
isProcessing = true;
deleteCount = 0;
uiManager.updateStatus('运行中...', true);
// [新增] 日志输出对应模式
let modeText = '全部删除';
if (mode === 'lottery') modeText = '筛选抽奖';
else if (mode === 'repost') modeText = '删除转发';
else if (mode === 'keyword') modeText = `关键词删除[${keyword}]`;
uiManager.log(`开始任务: ${modeText}`);
document.querySelectorAll('.cleaner-btn').forEach(b => b.disabled = true);
// [新增] 预处理关键词列表,不区分大小写
const keywords = keyword.split(/[,,\s]+/).map(s => s.trim().toLowerCase()).filter(Boolean);
let offset = '';
let hasMore = true;
let consecutiveErrors = 0;
try {
while (hasMore && isProcessing) {
let data;
try {
const res = await api.getSpaceHistory(offset);
if (res.code !== 0) throw new Error(res.message || ('code=' + res.code));
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;
}
// [修复] 新版字段
const items = (data && data.items) || [];
if (items.length === 0) break;
hasMore = !!data.has_more;
for (const item of items) {
if (!isProcessing) break;
const dynamicId = item.id_str; // [修复]
const type = item.type; // [修复] 字符串
const parsedContent = ContentParser.parse(item);
let shouldDelete = false;
if (mode === 'all') {
shouldDelete = true;
} else if (mode === 'repost') {
// [修复] 转发类型判断改为字符串,或存在 orig 字段
if (type === DYNAMIC_TYPES.FORWARD || item.orig) 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)}...`);
}
}
// [新增] 关键词匹配模式
else if (mode === 'keyword') {
// 将动态正文与转发原文合并,并转为小写以不区分大小写匹配
const textToCheck = (parsedContent.text + " " + parsedContent.origin).toLowerCase();
// 只要包含任意一个关键词,即触发删除
const hit = keywords.some(kw => textToCheck.includes(kw));
if (hit) {
shouldDelete = true;
}
}
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 || JSON.stringify(delRes)}`, true);
}
await api.sleep(CONFIG.INTERVAL);
} catch (e) {
uiManager.log(`API请求错误: ${e.message}`, true);
}
}
}
// [修复] 翻页游标用 data.offset,没有则终止
offset = data.offset || '';
if (!offset) hasMore = false;
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);
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 = '320px';
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');
};
// [新增] 绑定关键词删除事件
document.getElementById('btnDeleteKeyword').onclick = () => {
const input = prompt('请输入要匹配的关键词:\n(多个词可用逗号或空格分隔,不区分大小写)\n动态正文或转发原文包含任一关键词即删除:');
if (input !== null && input.trim() !== '') {
processDynamics('keyword', input.trim());
}
};
console.log('B站动态清理助手 v2.14 已加载');
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();