// ==UserScript== // @name B站全部取消关注(自动续跑) // @namespace codex.local // @version 1.0.0 // @description 使用B站关注页原生按钮分批取消关注,自动刷新续跑,遇到手机号验证自动暂停。 // @match https://space.bilibili.com/*/relation/follow* // @grant none // @run-at document-idle // ==/UserScript== (() => { "use strict"; if (window.__CODEX_BILI_UNFOLLOW_ALL__) return; window.__CODEX_BILI_UNFOLLOW_ALL__ = true; const STORAGE_KEY = "codex:bilibili-unfollow-all:v1"; const MIN_DELAY_MS = 600; const MAX_DELAY_MS = 900; const PAGE_SIZE = 24; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const readState = () => { try { return { running: false, success: 0, startedAt: null, ...JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"), }; } catch { return { running: false, success: 0, startedAt: null }; } }; const writeState = patch => { const next = { ...readState(), ...patch }; localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); return next; }; const isRunning = () => readState().running === true; const currentMid = () => { const match = location.pathname.match(/^\/(\d+)\/relation\/follow/); return match?.[1] || null; }; const getFollowButtons = () => [...document.querySelectorAll(".follow-btn__trigger.gray")].filter(el => (el.textContent || "").includes("已关注"), ); const isVisible = el => Boolean(el && (el.offsetWidth || el.offsetHeight || el.getClientRects().length)); const hasPhoneVerification = () => [...document.querySelectorAll("body *")].some( el => isVisible(el) && (el.textContent || "").trim() === "请输入您绑定的手机号码", ); const pressEscape = () => { for (const type of ["keydown", "keyup"]) { document.dispatchEvent( new KeyboardEvent(type, { key: "Escape", code: "Escape", keyCode: 27, which: 27, bubbles: true, }), ); } }; const identifyAccount = button => { let node = button; for (let i = 0; i < 8 && node; i++, node = node.parentElement) { const link = [...node.querySelectorAll('a[href*="follow.user_card.click"]')] .find(el => (el.textContent || "").trim()); if (!link) continue; const mid = (link.getAttribute("href") || "").match( /space\.bilibili\.com\/(\d+)/, )?.[1]; return { mid: mid || "unknown", name: (link.textContent || "").trim() || "未知账号", }; } return { mid: "unknown", name: "未知账号" }; }; const waitForButtonDecrease = async (before, timeoutMs = 1800) => { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { await sleep(100); const count = getFollowButtons().length; if (count === before - 1) return count; if (hasPhoneVerification()) return count; } return getFollowButtons().length; }; const fetchLiveTotal = async () => { const mid = currentMid(); if (!mid) return null; const params = new URLSearchParams({ order: "desc", order_type: "", vmid: mid, pn: "1", ps: "1", gaia_source: "main_web", web_location: "333.1387", }); try { const response = await fetch( `https://api.bilibili.com/x/relation/followings?${params}`, { credentials: "include" }, ); const result = await response.json(); return result.code === 0 ? Number(result.data?.total ?? 0) : null; } catch { return null; } }; const waitForPageReady = async (timeoutMs = 30000) => { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline && isRunning()) { if (hasPhoneVerification()) return "verification"; if (getFollowButtons().length > 0) return "ready"; const total = await fetchLiveTotal(); if (total === 0) return "complete"; await sleep(500); } return "timeout"; }; const panel = document.createElement("div"); panel.id = "codex-bili-unfollow-panel"; panel.style.cssText = [ "position:fixed", "right:20px", "bottom:20px", "z-index:2147483647", "width:280px", "padding:14px", "border-radius:10px", "background:rgba(25,25,25,.94)", "color:#fff", "font:13px/1.6 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif", "box-shadow:0 8px 28px rgba(0,0,0,.35)", ].join(";"); panel.innerHTML = `