// ==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/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 GM_addStyle // @downloadURL https://update.greasyfork.icu/scripts/520610/B%E7%AB%99%E5%8A%A8%E6%80%81%E6%89%B9%E9%87%8F%E5%88%A0%E9%99%A4%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/520610/B%E7%AB%99%E5%8A%A8%E6%80%81%E6%89%B9%E9%87%8F%E5%88%A0%E9%99%A4%E5%8A%A9%E6%89%8B.meta.js // ==/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 = `

快捷操作

其他

动态保留设置

输入动态中的部分内容即可,建议复制完整内容以提高匹配准确度
设置要保留的点赞最高动态数量
`; 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 = `
输入动态中的部分内容即可,建议复制完整内容以提高匹配准确度 `; // 添加删除按钮的事件监听 newItem.querySelector('.remove-content').addEventListener('click', () => { newItem.remove(); }); container.insertBefore(newItem, document.querySelector('.add-preserve-content')); } init(); })();