// ==UserScript==
// @name B站动态批量删除助手
// @version 0.28
// @description 这是一个帮助B站用户高效管理个人动态的脚本,支持多种类型动态的批量删除操作。
// @author 梦把我
// @match https://space.bilibili.com/*
// @match http://space.bilibili.com/*
// @require https://greasyfork.org/scripts/38220-mscststs-tools/code/MSCSTSTS-TOOLS.js?version=713767
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @icon https://static.hdslb.com/images/favicon.ico
// @namespace https://greasyfork.org/users/1383389
// @license MIT
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
const uid = window.location.pathname.split("/")[1];
function getUserCSRF() {
return document.cookie.split("; ").find(row => row.startsWith("bili_jct="))?.split("=")[1];
}
const csrfToken = getUserCSRF();
class Api {
constructor() { }
async spaceHistory(offset = 0) { // 获取个人动态
return this.retryOn429(() => this._api(
`https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history?visitor_uid=${uid}&host_uid=${uid}&offset_dynamic_id=${offset}`,
{}, "get"
));
}
async removeDynamic(id) { // 删除动态
return this._api(
"https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/rm_dynamic",
{ dynamic_id: id, csrf_token: csrfToken }
);
}
async _api(url, data, method = "post") { // 通用请求
return axios({
url,
method,
data: this.transformRequest(data),
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(res => res.data);
}
transformRequest(data) { // 转换请求参数
return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
}
async fetchJsonp(url) { // jsonp请求
return fetchJsonp(url).then(res => res.json());
}
async retryOn429(func, retries = 5, delay = 100) { // 出现429错误时冷却100ms重试,出现412错误时提示并退出
while (retries > 0) {
try {
return await func();
} catch (err) {
if (err.response && err.response.status === 429) {
await this.sleep(delay);
retries--;
} else if (err.response && err.response.status === 412) {
alert('由于请求过于频繁,IP暂时被ban,请更换IP或稍后再试。');
throw new Error('IP blocked, please retry later.');
} else {
throw err;
}
}
}
throw new Error('Too many retries, request failed.');
}
sleep(ms) { // 睡眠
return new Promise(resolve => setTimeout(resolve, ms));
}
}
const api = new Api();
const buttons = [
".onlyDeleteRepost",
".deleteVideo",
".deleteImage",
".deleteText",
".deleteArticle",
".deleteShortVideo"
];
let logNode;
// 添加确认状态管理
const confirmStates = {
deleteStates: {},
resetTimer: null
};
// 获取当前URL中的UID
function getCurrentUID() {
const pathParts = window.location.pathname.split('/');
return pathParts[1] || '';
}
// 获取自己的UID(通过访问space.bilibili.com)
async function getMyUID() {
try {
const response = await fetch('https://space.bilibili.com/', {
credentials: 'include' // 确保携带cookie
});
// 获取重定向后的URL
const redirectUrl = response.url;
const uid = redirectUrl.split('/').pop();
return uid;
} catch (error) {
console.error('获取用户UID失败:', error);
return null;
}
}
async function init() {
try {
// 等待页面加载完成
await new Promise(resolve => setTimeout(resolve, 500));
// 获取当前页面UID和自己的UID
const currentUID = getCurrentUID();
const myUID = await getMyUID();
// 判断是否为自己的空间
if (!currentUID || !myUID || currentUID !== myUID) {
console.log('当前不是自己的个人动态页面,脚本未启用');
return;
}
// 创建控制面板节点
const node = createControlPanel();
// 尝试插入到新版或旧版界面
try {
// 先尝试获取新版界面的位置
const newVersionContainer = document.querySelector("#app > main > div.space-dynamic > div.space-dynamic__right");
if (newVersionContainer) {
const firstChild = newVersionContainer.querySelector("div:nth-child(1)");
if (firstChild) {
newVersionContainer.insertBefore(node, firstChild);
} else {
newVersionContainer.appendChild(node);
}
console.log('成功插入到新版界面');
} else {
// 如果找不到新版界面,尝试旧版界面
const oldVersionContainer = document.querySelector("#page-dynamic .col-2");
if (oldVersionContainer) {
oldVersionContainer.appendChild(node);
console.log('成功插入到旧版界面');
} else {
console.error('无法找到合适的插入位置');
return;
}
}
// 设置事件监听
setEventListeners();
// 设置教程链接
document.querySelector('.tutorial-btn').href = 'https://www.bilibili.com/video/BV13NBnYyEML/';
// 添加样式
addConfirmationStyles();
} catch (error) {
console.error('插入控制面板失败:', error);
}
} catch (error) {
console.error('验证用户身份失败:', error);
}
}
function createControlPanel() {
const node = document.createElement("div");
node.className = "msc_panel";
node.innerHTML = `
<div class="inner">
<div class="panel-section quick-actions">
<h3>快捷操作</h3>
<div class="button-group">
<button class="onlyDeleteRepost">删除转发动态</button>
<button class="deleteVideo">删除视频动态</button>
<button class="deleteImage">删除图片动态</button>
<button class="deleteText">删除文字动态</button>
<button class="deleteArticle">删除专栏动态</button>
<button class="deleteShortVideo">删除小视频动态</button>
</div>
</div>
<div class="panel-section other-actions">
<h3>其他</h3>
<div class="button-group">
<a href="#" class="tutorial-btn" target="_blank">使用视频教程</a>
</div>
</div>
<div class="panel-section pin-settings">
<h3>动态保留设置</h3>
<div class="setting-group">
<label class="switch">
<input type="checkbox" id="preservePinned">
<span class="slider round"></span>
<span class="label">保留指定的动态</span>
</label>
<div class="preserve-contents">
<div class="preserve-content-item">
<input type="text" class="preserve-content" placeholder="输入要保留的动态文字内容">
<small class="tip">输入动态中的部分内容即可,建议复制完整内容以提高匹配准确度</small>
</div>
<button class="add-preserve-content" title="添加更多保留内容">
<span>+</span>
</button>
</div>
<div class="most-liked-setting">
<label class="switch">
<input type="checkbox" id="preserveMostLiked">
<span class="slider round"></span>
<span class="label">保留点赞最高的动态</span>
</label>
<div class="most-liked-count">
<input type="number" id="mostLikedCount" value="3" min="1" disabled>
<small class="tip">设置要保留的点赞最高动态数量</small>
</div>
</div>
</div>
</div>
</div>`;
return node;
}
function setEventListeners() {
document.querySelector(".onlyDeleteRepost").addEventListener("click", () => handleConfirmation("onlyDeleteRepost", () => handleDelete(false)));
document.querySelector(".deleteVideo").addEventListener("click", () => handleConfirmation("deleteVideo", () => handleDeleteByType(8)));
document.querySelector(".deleteImage").addEventListener("click", () => handleConfirmation("deleteImage", () => handleDeleteByType(2)));
document.querySelector(".deleteText").addEventListener("click", () => handleConfirmation("deleteText", () => handleDeleteByType(4)));
document.querySelector(".deleteArticle").addEventListener("click", () => handleConfirmation("deleteArticle", () => handleDeleteByType(64)));
document.querySelector(".deleteShortVideo").addEventListener("click", () => handleConfirmation("deleteShortVideo", () => handleDeleteByType(16)));
// 添加新的保留内容输入框
document.querySelector('.add-preserve-content').addEventListener('click', addPreserveContentInput);
// 添加点赞设置相关的事件监听
const preserveMostLikedCheckbox = document.querySelector('#preserveMostLiked');
const mostLikedCountInput = document.querySelector('#mostLikedCount');
preserveMostLikedCheckbox.addEventListener('change', (e) => {
mostLikedCountInput.disabled = !e.target.checked;
});
mostLikedCountInput.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
if (value < 1) e.target.value = 1;
});
}
async function handleDelete(deleteLottery) { // 删除参数 unfollow
disableAll();
let deleteCount = 0; // 删除计数
let hasMore = true; // 是否还有更多动态
let offset = 0; // 动态偏移量
while (hasMore) {
const { data } = await api.spaceHistory(offset);
hasMore = data.has_more;
for (const card of data.cards) {
offset = card.desc.dynamic_id_str;
if (card.desc.orig_dy_id != 0) { // 如果是转发动态
try {
const content = JSON.parse(card.card);
const content2 = JSON.parse(content.origin_extend_json);
if (!deleteLottery || content2.lott) { // 如果"仅删除抽奖"为假,或判断为抽奖动态
const rm = await api.removeDynamic(card.desc.dynamic_id_str);
if (rm.code === 0) deleteCount++;
else throw new Error("删除出错");
}
await api.sleep(50);
log(`已删除 ${deleteCount} 条动态`);
} catch (e) {
console.error(e);
break;
}
}
}
}
enableAll();
}
function disableAll() {
console.log('start');
buttons.forEach(btn => {
const button = document.querySelector(btn);
button.disabled = true;
resetButtonState(btn.substring(1)); // 移除开头的点号
});
confirmStates.deleteStates = {}; // 清除所有确认状态
}
function enableAll() {
console.log('done');
buttons.forEach(btn => {
const button = document.querySelector(btn);
if (button) {
button.disabled = false;
resetButtonState(btn.substring(1));
}
});
confirmStates.deleteStates = {};
log('操作已完成!', true);
}
let currentPopup = null;
let currentTimer = null;
function log(message, autoRefresh = false) {
// 如果存在之前的弹窗和定时器,先清除
if (currentPopup) {
currentPopup.remove();
clearTimeout(currentTimer);
}
// 创建新的弹窗
const popup = document.createElement('div');
popup.className = 'log-popup';
popup.textContent = message;
document.body.appendChild(popup);
currentPopup = popup;
if (autoRefresh) {
let countdown = 3;
const updateCountdown = () => {
popup.textContent = `${message} (${countdown}秒后自动刷新)`;
countdown--;
if (countdown < 0) {
window.location.reload();
} else {
currentTimer = setTimeout(updateCountdown, 1000);
}
};
updateCountdown();
} else {
// 3秒后自动隐藏弹窗
currentTimer = setTimeout(() => {
popup.classList.add('hide');
setTimeout(() => popup.remove(), 300);
}, 3000);
}
}
async function handleDeleteByType(targetType) {
const preservePinned = document.querySelector('#preservePinned').checked;
const preserveMostLiked = document.querySelector('#preserveMostLiked').checked;
const mostLikedCount = parseInt(document.querySelector('#mostLikedCount').value);
const preserveContents = Array.from(document.querySelectorAll('.preserve-content'))
.map(input => input.value.trim())
.filter(value => value !== '');
if (preservePinned && preserveContents.length === 0) {
alert('检测到开启保留动态功能,请至少输入一个要保留的动态内容');
return;
}
try {
disableAll();
let deleteCount = 0;
let hasMore = true;
let offset = 0;
let allDynamics = [];
// 首先收集所有动态
while (hasMore) {
const { data } = await api.spaceHistory(offset);
hasMore = data.has_more;
for (const card of data.cards) {
if (card.desc.type === targetType) {
allDynamics.push({
id: card.desc.dynamic_id_str,
likes: card.desc.like,
content: JSON.parse(card.card)?.item?.content || ''
});
}
offset = card.desc.dynamic_id_str;
}
}
// 如果需要保留点赞最高的动态
let preservedIds = new Set();
if (preserveMostLiked && mostLikedCount > 0) {
const topLiked = allDynamics
.sort((a, b) => b.likes - a.likes)
.slice(0, mostLikedCount);
preservedIds = new Set(topLiked.map(d => d.id));
}
// 执行删除操作
for (const dynamic of allDynamics) {
// 跳过需要保留的动态
if (preservedIds.has(dynamic.id)) {
console.log('保留点赞数最高的动态:', dynamic.id, '点赞数:', dynamic.likes);
continue;
}
// 检查是否包含需要保留的内容
if (preservePinned && preserveContents.length > 0) {
if (preserveContents.some(content => dynamic.content.includes(content))) {
console.log('跳过包含保留内容的动态:', dynamic.content);
continue;
}
}
try {
const rm = await api.removeDynamic(dynamic.id);
if (rm.code === 0) deleteCount++;
await api.sleep(50);
log(`已删除 ${deleteCount} 条类型为 ${targetType} 的动态`);
} catch (e) {
console.error(e);
break;
}
}
} catch (error) {
console.error('删除操作执行出错:', error);
} finally {
enableAll();
}
}
// 添加确认处理函数
function handleConfirmation(buttonId, callback) {
const button = document.querySelector(`.${buttonId}`);
if (!button) return;
const originalText = button.textContent;
// 如果是首次点击
if (!confirmStates.deleteStates[buttonId]) {
// 设置确认状态
confirmStates.deleteStates[buttonId] = true;
// 修改按钮文字
button.textContent = "确认删除?";
button.style.backgroundColor = "#ff6b6b";
// 添加闪烁动画
button.style.animation = "buttonBlink 1s infinite";
// 5秒后重置状态
setTimeout(() => {
resetButtonState(buttonId);
}, 5000);
// 显示提示
log("请再次点击确认删除操作");
} else {
try {
// 第二次点击,执行删除
resetButtonState(buttonId);
callback();
} catch (error) {
console.error('执行删除操作时出错:', error);
resetButtonState(buttonId);
enableAll();
log('操作执行出错,请重试');
}
}
}
// 重置按钮状态
function resetButtonState(buttonId) {
const button = document.querySelector(`.${buttonId}`);
if (!button) return;
// 重置确认状态
confirmStates.deleteStates[buttonId] = false;
// 重置按钮状态
button.disabled = false;
button.textContent = getOriginalButtonText(buttonId);
button.style.backgroundColor = "";
button.style.animation = "";
// 清除可能存在的定时器
if (confirmStates.resetTimer) {
clearTimeout(confirmStates.resetTimer);
confirmStates.resetTimer = null;
}
}
// 获取按钮原始文字
function getOriginalButtonText(buttonId) {
const textMap = {
'onlyDeleteRepost': '删除转发动态',
'deleteVideo': '删除视频动态',
'deleteImage': '删除图片动态',
'deleteText': '删除文字动态',
'deleteArticle': '删除专栏动态',
'deleteShortVideo': '删除小视频动态'
};
return textMap[buttonId] || '删除';
}
// 添加闪烁动画样式
function addConfirmationStyles() {
// 使用 GM_addStyle 替代直接创建 style 标签
const styles = `
.msc_panel {
max-width: 100%;
margin: 0 0 20px 0;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.panel-section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.panel-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.panel-section h3 {
font-size: 16px;
color: #18191c;
margin-bottom: 16px;
font-weight: 500;
}
.type-table table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
font-size: 14px;
}
.type-table th, .type-table td {
padding: 8px;
text-align: center;
border: 1px solid #eee;
}
.type-table th {
background: #f6f7f8;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.type-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.msc_panel button {
padding: 8px 16px;
border-radius: 4px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.msc_panel button:hover {
background: #f6f7f8;
}
.msc_panel button.primary-btn {
background: #00aeec;
color: #fff;
border-color: #00aeec;
}
.msc_panel button.primary-btn:hover {
background: #0096cc;
}
.msc_panel button.warning-btn {
background: #fb7299;
color: #fff;
border-color: #fb7299;
}
.msc_panel button.warning-btn:hover {
background: #e45c80;
}
.msc_panel button:disabled {
background: #eee;
color: #999;
cursor: not-allowed;
border-color: #ddd;
}
.tutorial-btn {
display: inline-block;
padding: 8px 16px;
background: #6c757d;
color: #fff;
text-decoration: none;
border-radius: 4px;
transition: all 0.2s;
}
.tutorial-btn:hover {
background: #5a6268;
}
.log {
margin-top: 16px;
padding: 12px;
background: #f6f7f8;
border-radius: 4px;
font-size: 14px;
color: #666;
}
@keyframes buttonBlink {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
.msc_panel button.confirming {
background-color: #ff6b6b !important;
color: white !important;
}
.msc_panel button:disabled {
animation: none !important;
opacity: 0.5 !important;
}
.pin-settings {
margin: 15px 0;
}
.setting-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.switch {
display: flex;
align-items: center;
gap: 10px;
}
.switch input {
display: none;
}
.slider {
position: relative;
width: 40px;
height: 20px;
background-color: #ccc;
border-radius: 20px;
cursor: pointer;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 50%;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(20px);
}
.pin-content-input {
margin-top: 5px;
}
.pin-content-input input {
width: 100%;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tip {
color: #999;
font-size: 12px;
margin-top: 5px;
display: block;
}
/* 弹窗样式 */
.log-popup {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 999999;
font-size: 14px;
max-width: 300px;
animation: fadeInOut 0.3s ease-in-out;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.log-popup.hide {
animation: fadeOut 0.3s ease-in-out forwards;
}
@keyframes fadeOut {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(20px);
}
}
.preserve-contents {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.preserve-content-item {
position: relative;
}
.input-wrapper {
display: flex;
gap: 8px;
align-items: center;
}
.preserve-content {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
}
.add-preserve-content {
align-self: flex-start;
padding: 4px 12px;
background: #00aeec;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-top: 5px;
}
.add-preserve-content:hover {
background: #0096cc;
}
.remove-content {
padding: 4px 8px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.remove-content:hover {
background: #ff5252;
}
.most-liked-setting {
margin-top: 15px;
}
.most-liked-count {
margin-top: 10px;
margin-left: 30px;
}
.most-liked-count input {
width: 60px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.most-liked-count input:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
`;
// 添加 @grant GM_addStyle 到脚本头部后,使用 GM_addStyle
GM_addStyle(styles);
}
// 添加动态类型验证函数
function isValidDynamicType(type) {
const validTypes = [1, 2, 4, 8, 16, 64];
return validTypes.includes(type);
}
// 检查是否存在置顶动态
async function checkPinnedDynamic() {
try {
// 检查新版界面
const newVersionPin = document.evaluate(
'//*[@id="app"]/main/div[1]/div[2]/div/div/div/div[1]/div[1]/div/div[1]/div/div',
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
// 检查旧版界面
const oldVersionPin = document.evaluate(
'//*[@id="page-dynamic"]/div[1]/div/div[1]/div/div/div[1]/div/div',
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
return !!(newVersionPin || oldVersionPin);
} catch (error) {
console.error('检查置顶动态失败:', error);
return false;
}
}
// 添加新的保留内容输入框的函数
function addPreserveContentInput() {
const container = document.querySelector('.preserve-contents');
const newItem = document.createElement('div');
newItem.className = 'preserve-content-item';
newItem.innerHTML = `
<div class="input-wrapper">
<input type="text" class="preserve-content" placeholder="输入要保留的动态文字内容">
<button class="remove-content" title="删除此条件">×</button>
</div>
<small class="tip">输入动态中的部分内容即可,建议复制完整内容以提高匹配准确度</small>
`;
// 添加删除按钮的事件监听
newItem.querySelector('.remove-content').addEventListener('click', () => {
newItem.remove();
});
container.insertBefore(newItem, document.querySelector('.add-preserve-content'));
}
init();
})();