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