// ==UserScript== // @name B站动态批量删除小工具 - 交互版 // @version 2.1 // @description B站动态批量删除工具,支持在删除抽奖动态时预览内容并逐条确认 // @author Gemini // @match https://space.bilibili.com/* // @match http://space.bilibili.com/* // @icon https://static.hdslb.com/images/favicon.ico // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ================= 配置区域 ================= const CONFIG = { // 请求间隔 (毫秒),太快会被B站拦截 INTERVAL: 800, // 如果失败,重试次数 RETRY_LIMIT: 3, AUTO_RELOAD: false // 改为 false 就不会自动刷新了 }; // 动态类型映射 const DYNAMIC_TYPES = { REPOST: 1, IMAGE: 2, TEXT: 4, VIDEO: 8, SHORT_VIDEO: 16, ARTICLE: 64 }; // 获取UID和CSRF Token const uid = window.location.pathname.split("/")[1]; function getCSRFToken() { const match = document.cookie.match(/bili_jct=([^;]+)/); return match ? match[1] : ''; } const csrfToken = getCSRFToken(); // ================= 工具类:解析动态内容 ================= class ContentParser { static parse(cardStr, desc) { try { const card = JSON.parse(cardStr); let content = "无法解析内容"; let originContent = ""; // 1. 提取自己的评论/文字 if (card.item && card.item.content) { content = card.item.content; } else if (card.item && card.item.description) { content = card.item.description; } else if (desc && desc.dynamic_id_str) { content = "(无文字内容)"; } // 2. 如果是转发,提取原动态信息 if (card.origin) { try { const origin = JSON.parse(card.origin); let originText = ""; if (origin.item && (origin.item.content || origin.item.description)) { originText = origin.item.content || origin.item.description; } else if (origin.title) { originText = origin.title; } else if (origin.desc) { originText = origin.desc; } const author = origin.user ? origin.user.name : (origin.owner ? origin.owner.name : "未知用户"); originContent = `[原动态 @${author}]: ${originText}`; } catch (e) { originContent = "[原动态已失效或无法解析]"; } } return { text: content, origin: originContent, isLottery: this.checkIsLottery(card) }; } catch (e) { console.error("解析动态失败", e); return { text: "解析数据格式错误", origin: "", isLottery: false }; } } static checkIsLottery(card) { // 检查标准抽奖字段 if (card.origin_extension && card.origin_extension.lott) return true; // 检查扩展字段 if (card.origin_extend_json) { try { const ext = JSON.parse(card.origin_extend_json); if (ext.lott) return true; } catch (e) {} } return false; } } // ================= API类 ================= class DynamicAPI { constructor() { this.baseUrl = 'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr'; } async getSpaceHistory(offset = 0) { const url = `${this.baseUrl}/space_history?visitor_uid=${uid}&host_uid=${uid}&offset_dynamic_id=${offset}`; try { const response = await fetch(url, { credentials: 'include' }); return await response.json(); } catch (error) { console.error('获取动态失败:', error); throw error; } } async deleteDynamic(dynamicId) { const url = `${this.baseUrl}/rm_dynamic`; const formData = new URLSearchParams(); formData.append('dynamic_id', dynamicId); formData.append('csrf_token', csrfToken); try { const response = await fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData }); return await response.json(); } catch (error) { console.error('删除动态失败:', error); throw error; } } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } const api = new DynamicAPI(); let isProcessing = false; let deleteCount = 0; // ================= UI管理器 ================= class UIManager { constructor() { this.panel = null; this.resultBox = null; this.modal = null; } createPanel() { const panel = document.createElement('div'); panel.className = 'bili-cleaner-panel'; panel.innerHTML = `

🛡️ 动态清理助手 v2.0

就绪
等待操作...
`; 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,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; } .action-buttons { display: flex; gap: 10px; margin-bottom: 15px; } .cleaner-btn { flex: 1; padding: 8px 0; border: 1px solid #e7e7e7; background: #f9f9f9; border-radius: 6px; cursor: pointer; transition: all 0.2s; font-size: 13px; color: #333; } .cleaner-btn:hover { border-color: #00a1d6; color: #00a1d6; background: #eef8fc; } .cleaner-btn:disabled { opacity: 0.6; cursor: not-allowed; } .result-box { background: #f4f5f7; border-radius: 6px; padding: 10px; font-size: 12px; color: #555; max-height: 100px; overflow-y: auto; } /* 弹窗样式 */ .cleaner-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,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,0.2); animation: popIn 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28); } .modal-title { font-size: 16px; font-weight: bold; margin-bottom: 15px; color: #333; display: flex; justify-content: space-between; } .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: white; } .btn-skip { background: #e7e7e7; color: #333; } .btn-stop { background: #333; color: white; } .btn-confirm-del:hover { background: #ff3333; } .btn-skip:hover { background: #d7d7d7; } @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } `; document.head.appendChild(style); } updateStatus(text, isRunning = false) { const el = document.getElementById('statusIndicator'); if (el) { el.textContent = text; el.className = `status-indicator ${isRunning ? 'running' : ''}`; } } log(msg, isError = false) { const box = document.querySelector('.result-content'); if (box) { const time = new Date().toTimeString().split(' ')[0]; const p = document.createElement('div'); p.style.color = isError ? '#ff4d4d' : '#333'; p.textContent = `[${time}] ${msg}`; box.insertBefore(p, box.firstChild); } } // 显示交互弹窗 (返回 Promise,等待用户点击) async showConfirmModal(parsedData) { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.className = 'cleaner-modal-overlay'; overlay.innerHTML = `
`; document.body.appendChild(overlay); const cleanup = () => { document.body.removeChild(overlay); }; document.getElementById('c_del').onclick = () => { cleanup(); resolve('delete'); }; document.getElementById('c_skip').onclick = () => { cleanup(); resolve('skip'); }; document.getElementById('c_stop').onclick = () => { cleanup(); resolve('stop'); }; }); } } // ================= 核心逻辑 ================= const uiManager = new UIManager(); // 通用删除处理器 async function processDynamics(mode) { if (isProcessing) return; isProcessing = true; deleteCount = 0; uiManager.updateStatus('运行中...', true); uiManager.log(`开始任务: ${mode === 'lottery' ? '筛选抽奖' : (mode === 'repost' ? '删除转发' : '全部删除')}`); // 禁用按钮 document.querySelectorAll('.cleaner-btn').forEach(b => b.disabled = true); let offset = 0; let hasMore = true; let consecutiveErrors = 0; try { while (hasMore && isProcessing) { // 1. 获取历史记录 let data; try { const res = await api.getSpaceHistory(offset); if (res.code !== 0) throw new Error(res.message); 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; } if (!data || !data.cards || data.cards.length === 0) break; hasMore = data.has_more; // 2. 遍历当前页的卡片 for (const card of data.cards) { if (!isProcessing) break; // 更新offset为当前card的id,用于下次翻页(非常重要,否则会死循环) offset = card.desc.dynamic_id_str; const dynamicId = card.desc.dynamic_id_str; const cardStr = card.card; const type = card.desc.type; const origId = card.desc.orig_dy_id; let shouldDelete = false; let parsedContent = null; // 解析内容 parsedContent = ContentParser.parse(cardStr, card.desc); // --- 模式判断 --- if (mode === 'all') { shouldDelete = true; } else if (mode === 'repost') { // 转发类型为1,或者 orig_dy_id 不为0 if (type === DYNAMIC_TYPES.REPOST || origId !== 0) 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)}...`); } } } // --- 执行删除 --- 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}`, true); } // 成功删除后稍微等待,避免频繁请求 await api.sleep(CONFIG.INTERVAL); } catch (e) { uiManager.log(`API请求错误: ${e.message}`, true); } } } // 翻页间隙 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); // 3秒后刷新 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 = '300px'; 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'); }; console.log('B站动态清理助手交互版已加载'); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();