// ==UserScript==
// @name B站动态批量删除助手
// @version 0.25
// @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/axios@1.7.3/dist/axios.min.js
// @icon https://static.hdslb.com/images/favicon.ico
// @namespace https://greasyfork.org/users/1383389
// @license MIT
// @grant none
// @downloadURL none
// ==/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", ".deleteCustomType"];
let logNode;
// 添加确认状态管理
const confirmStates = {
deleteStates: {},
resetTimer: null
};
async function init() {
try {
// 等待页面加载完成
await new Promise(resolve => setTimeout(resolve, 500));
// 检查是否为新版界面的个人空间
const announcementText = document.querySelector("#app > main > div.space-dynamic > div.space-dynamic__right > div:nth-child(6) > div > div.ann-section__content > div > div.show-wrap > div > p");
const isNewVersionMySpace = announcementText && announcementText.textContent === "编辑我的公告";
// 检查是否为旧版界面的个人空间
const oldVersionText = await mscststs.wait(".h-version-state", true, 100);
const isOldVersionMySpace = oldVersionText && oldVersionText.innerText === "我自己";
// 如果既不是新版也不是旧版的个人空间,则退出
if (!isNewVersionMySpace && !isOldVersionMySpace) {
console.log('当前不是自己的个人动态页面,脚本未启用');
return;
}
// 创建控制面板节点
const node = createControlPanel();
// 尝试插入到新版或旧版界面
try {
if (isNewVersionMySpace) {
// 新版界面插入位置
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 {
console.error('无法找到新版界面插入位置');
return;
}
} 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 = '占位符URL';
// 添加样式
addConfirmationStyles();
} catch (error) {
console.error('插入控制面板失败:', error);
}
} catch (error) {
console.error('验证用户身份失败:', error);
}
}
function createControlPanel() {
const node = document.createElement("div");
node.className = "msc_panel";
node.innerHTML = `
动态类型对照表
类型值 | 含义 |
1 | 转发 |
2 | 图片动态 |
4 | 文字动态 |
8 | 视频动态 |
16 | 小视频 |
64 | 专栏 |
快捷操作
`;
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(".deleteCustomType").addEventListener("click", () => {
const typeValue = parseInt(document.querySelector(".type-input").value);
if (isValidDynamicType(typeValue)) {
handleConfirmation("deleteCustomType", () => handleDeleteByType(typeValue));
} else {
alert("请输入有效的动态类型值!\n有效值包括:1(转发)、2(图片)、4(文字)、8(视频)、16(小视频)、64(专栏)");
resetButtonState("deleteCustomType");
enableAll();
}
});
}
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 pinnedContent = document.querySelector('#pinnedContent').value.trim();
if (preservePinned && !pinnedContent && await checkPinnedDynamic()) {
alert('检测到开启保护置顶功能,请输入置顶动态内容关键词以保护置顶动态');
return;
}
try {
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.type === targetType) {
// 检查是否为置顶动态
if (preservePinned && pinnedContent) {
const cardContent = JSON.parse(card.card);
const content = cardContent.item?.content || '';
if (content.includes(pinnedContent)) {
console.log('跳过置顶动态:', content);
continue;
}
}
try {
const rm = await api.removeDynamic(card.desc.dynamic_id_str);
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': '删除文字动态',
'deleteCustomType': '删除'
};
return textMap[buttonId] || '删除';
}
// 添加闪烁动画样式
function addConfirmationStyles() {
const style = document.createElement('style');
style.textContent = `
.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);
}
}
`;
document.head.appendChild(style);
}
// 添加动态类型验证函数
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;
}
}
init();
})();