取关脚本
x
(async () => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
let unfollowed = 0;
let rounds = 0;
const maxUnfollow = 100; // 一次最多取关多少,自己改小一点更安全
function getButtons() {
const buttons = [...document.querySelectorAll('button')];
return buttons.filter(btn => {
const text = btn.innerText.trim();
return ['Following', '正在关注', '已关注'].includes(text);
});
}
while (unfollowed < maxUnfollow && rounds < 50) {
const buttons = getButtons();
for (const btn of buttons) {
if (unfollowed >= maxUnfollow) break;
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(1500);
btn.click();
await sleep(1000);
const confirmBtn = [...document.querySelectorAll('button')].find(b => {
const t = b.innerText.trim();
return ['Unfollow', '取消关注'].includes(t);
});
if (confirmBtn) {
confirmBtn.click();
unfollowed++;
console.log(`已取关: ${unfollowed}`);
await sleep(3000 + Math.random() * 4000); // 随机慢一点
}
}
window.scrollBy(0, 1200);
await sleep(3000);
rounds++;
}
console.log(`完成,本次共取关 ${unfollowed} 个`);
})();youtube
(async () => {
const wait = ms => new Promise(r => setTimeout(r, ms));
const log = (...args) => console.log("[YT-UNSUB]", ...args);
const TEXT_SUBBED = ["已订阅", "已訂閱", "subscribed"];
const TEXT_UNSUB = ["退订", "退訂", "取消订阅", "取消訂閱", "unsubscribe"];
const TEXT_CONFIRM = ["退订", "退訂", "取消订阅", "取消訂閱", "unsubscribe"];
function norm(s) {
return (s || "").replace(/\s+/g, " ").trim().toLowerCase();
}
function visible(el) {
if (!el) return false;
const r = el.getBoundingClientRect();
const st = getComputedStyle(el);
return r.width > 0 && r.height > 0 && st.display !== "none" && st.visibility !== "hidden";
}
function hasAny(text, arr) {
const t = norm(text);
return arr.some(x => t.includes(x.toLowerCase()));
}
function getRows() {
return [...document.querySelectorAll("ytd-channel-renderer")];
}
function getName(row) {
return row.querySelector("#main-link")?.textContent?.trim() || "未知频道";
}
function getSubscribedBtn(row) {
return [...row.querySelectorAll("button, tp-yt-paper-button, yt-button-shape button")]
.find(btn => visible(btn) && hasAny(btn.innerText || btn.textContent || "", TEXT_SUBBED));
}
function getVisibleActionEls() {
return [...document.querySelectorAll(`
tp-yt-paper-item,
ytd-menu-service-item-renderer,
button,
tp-yt-paper-button,
div[role="menuitem"],
div[role="button"],
yt-formatted-string
`)].filter(visible);
}
function findUnsubEntry() {
const els = getVisibleActionEls();
return els.find(el => {
const txt = norm(el.innerText || el.textContent || "");
const aria = norm(el.getAttribute("aria-label") || "");
return TEXT_UNSUB.some(k => txt.includes(k.toLowerCase()) || aria.includes(k.toLowerCase()));
});
}
function findConfirmBtn() {
const els = [...document.querySelectorAll(`
tp-yt-paper-dialog button,
tp-yt-paper-dialog tp-yt-paper-button,
ytd-popup-container button,
ytd-popup-container tp-yt-paper-button,
button,
tp-yt-paper-button
`)].filter(visible);
return els.find(el => {
const txt = norm(el.innerText || el.textContent || "");
const aria = norm(el.getAttribute("aria-label") || "");
return TEXT_CONFIRM.some(k => txt.includes(k.toLowerCase()) || aria.includes(k.toLowerCase()));
});
}
async function pressEsc() {
document.dispatchEvent(new KeyboardEvent("keydown", {
key: "Escape",
code: "Escape",
keyCode: 27,
which: 27,
bubbles: true
}));
await wait(500);
}
async function autoScrollLoad(rounds = 25) {
let lastHeight = 0;
for (let i = 0; i < rounds; i++) {
window.scrollTo(0, document.documentElement.scrollHeight);
await wait(1500);
const h = document.documentElement.scrollHeight;
log(`滚动加载 ${i + 1}/${rounds}`);
if (h === lastHeight) break;
lastHeight = h;
}
window.scrollTo(0, 0);
await wait(1000);
}
async function handleRow(row) {
const name = getName(row);
const btn = getSubscribedBtn(row);
if (!btn) {
log("跳过,没找到已订阅按钮:", name);
return false;
}
row.scrollIntoView({ behavior: "smooth", block: "center" });
await wait(700);
log("正在处理:", name);
btn.click();
await wait(1200);
let unsub = findUnsubEntry();
if (!unsub) {
await wait(1200);
unsub = findUnsubEntry();
}
if (!unsub) {
log("没找到“退订”入口:", name);
await pressEsc();
return false;
}
unsub.click();
await wait(1200);
let confirm = findConfirmBtn();
if (!confirm) {
await wait(1000);
confirm = findConfirmBtn();
}
if (confirm) {
confirm.click();
log("已退订:", name);
await wait(1800);
return true;
}
log("未找到确认按钮,可能已直接退订:", name);
await wait(1200);
return true;
}
async function run(limit = 30) {
await autoScrollLoad();
let done = 0;
const rows = getRows();
for (const row of rows) {
if (done >= limit) break;
const ok = await handleRow(row);
if (ok) done++;
await wait(1000);
}
log(`本轮结束,共退订 ${done} 个频道`);
}
await run(30); // 可改成 10、20、50
})();pixiv
(async () => {
const STORAGE_KEY = '__pixiv_auto_unfollow__';
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
function saveState(state) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function loadState() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
} catch {
return null;
}
}
function clearState() {
localStorage.removeItem(STORAGE_KEY);
}
function isVisible(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function getFollowButtons() {
return [...document.querySelectorAll('button.charcoal-button')]
.filter(el => {
const text = (el.innerText || el.textContent || '').trim();
return text === '已关注' && !el.disabled && isVisible(el);
});
}
function getAllFollowButtons() {
return [...document.querySelectorAll('button.charcoal-button')]
.filter(el => {
const text = (el.innerText || el.textContent || '').trim();
return text === '已关注' && !el.disabled;
});
}
function clickLikeHuman(el) {
['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
el.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window
}));
});
}
function findConfirmButton() {
const texts = ['取消关注', '解除关注', '确认', '確認', 'OK', 'Unfollow', 'フォロー解除'];
return [...document.querySelectorAll('button, a, [role="button"], div')]
.find(el => texts.includes((el.innerText || el.textContent || '').trim()));
}
async function unfollowOne(btn) {
const beforeText = (btn.innerText || btn.textContent || '').trim();
if (beforeText !== '已关注') return false;
const beforeCount = getAllFollowButtons().length;
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(700);
try {
btn.click();
} catch {
clickLikeHuman(btn);
}
await sleep(1200);
let confirmBtn = findConfirmButton();
if (confirmBtn) {
try {
confirmBtn.click();
} catch {
clickLikeHuman(confirmBtn);
}
await sleep(1800);
}
let afterCount = getAllFollowButtons().length;
if (afterCount < beforeCount) return true;
clickLikeHuman(btn);
await sleep(1200);
confirmBtn = findConfirmButton();
if (confirmBtn) {
try {
confirmBtn.click();
} catch {
clickLikeHuman(confirmBtn);
}
await sleep(1800);
}
afterCount = getAllFollowButtons().length;
return afterCount < beforeCount;
}
function getCurrentPageNumber() {
const current = document.querySelector('span.charcoal-pagination-button[aria-current="page"]');
if (!current) return null;
const text = (current.innerText || current.textContent || '').trim();
return /^\d+$/.test(text) ? parseInt(text, 10) : null;
}
function getNextPageLink() {
const currentNum = getCurrentPageNumber();
if (!currentNum) return null;
return [...document.querySelectorAll('a.charcoal-pagination-button')]
.find(el => {
const text = (el.innerText || el.textContent || '').trim();
return /^\d+$/.test(text) && parseInt(text, 10) === currentNum + 1;
});
}
async function run() {
let state = loadState();
if (!state || !state.running) {
state = {
running: true,
total: 0,
startedAt: new Date().toISOString()
};
saveState(state);
}
const currentPage = getCurrentPageNumber() || '?';
console.log(`开始处理第 ${currentPage} 页,累计已取消 ${state.total} 个`);
let failStreak = 0;
while (true) {
const buttons = getFollowButtons();
if (!buttons.length) {
console.log(`第 ${currentPage} 页没有“已关注”按钮了`);
break;
}
const ok = await unfollowOne(buttons[0]);
if (ok) {
state.total++;
failStreak = 0;
saveState(state);
console.log(`累计已取消 ${state.total} 个`);
} else {
failStreak++;
console.warn(`取消失败 ${failStreak} 次`);
if (failStreak >= 3) break;
}
await sleep(1200 + Math.random() * 1000);
}
const nextLink = getNextPageLink();
if (!nextLink) {
console.log(`全部完成,总共取消 ${state.total} 个`);
clearState();
return;
}
console.log(`跳转到下一页:${nextLink.href}`);
await sleep(1500);
location.href = nextLink.href;
}
window.__pixivAutoUnfollowRun = run;
window.__pixivAutoUnfollowStop = () => {
clearState();
console.log('已停止自动取消关注');
};
await run();
})();pixiv 油猴版
// ==UserScript==
// @name Pixiv 自动取消关注(Following 分页版)
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 在 Pixiv following 页面自动取消关注,并自动翻页继续执行
// @match https://www.pixiv.net/users/*/following*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = '__pixiv_auto_unfollow_state__';
const CONFIG = {
startDelay: 2500,
betweenClicks: 1200,
betweenPages: 2500,
modalWait: 1400,
confirmWait: 1800,
failLimit: 3,
jitter: 1000
};
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
function rand(base) {
return base + Math.floor(Math.random() * CONFIG.jitter);
}
function saveState(state) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function loadState() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
} catch (e) {
return null;
}
}
function clearState() {
localStorage.removeItem(STORAGE_KEY);
}
function getState() {
let state = loadState();
if (!state) {
state = {
running: true,
total: 0,
pageDone: 0,
startedAt: new Date().toISOString(),
lastPage: null
};
saveState(state);
}
return state;
}
function setState(patch) {
const next = { ...getState(), ...patch };
saveState(next);
renderPanel(next);
return next;
}
function isVisible(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return (
rect.width > 0 &&
rect.height > 0 &&
style.visibility !== 'hidden' &&
style.display !== 'none'
);
}
function textOf(el) {
return (el?.innerText || el?.textContent || '').trim();
}
function clickLikeHuman(el) {
['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
el.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window
}));
});
}
function getCurrentPageNumber() {
const current = document.querySelector('span.charcoal-pagination-button[aria-current="page"]');
if (!current) return null;
const text = textOf(current);
return /^\d+$/.test(text) ? parseInt(text, 10) : null;
}
function getNextPageLink() {
const currentNum = getCurrentPageNumber();
if (!currentNum) return null;
return [...document.querySelectorAll('a.charcoal-pagination-button')]
.find(el => {
const text = textOf(el);
return /^\d+$/.test(text) && parseInt(text, 10) === currentNum + 1;
});
}
function getFollowButtons() {
return [...document.querySelectorAll('button.charcoal-button')]
.filter(el => textOf(el) === '已关注' && !el.disabled && isVisible(el));
}
function getAllFollowButtons() {
return [...document.querySelectorAll('button.charcoal-button')]
.filter(el => textOf(el) === '已关注' && !el.disabled);
}
function findConfirmButton() {
const texts = ['取消关注', '解除关注', '确认', '確認', 'OK', 'Unfollow', 'フォロー解除'];
return [...document.querySelectorAll('button, a, [role="button"], div')]
.find(el => texts.includes(textOf(el)) && isVisible(el));
}
async function waitForPageReady() {
for (let i = 0; i < 30; i++) {
if (document.querySelector('button.charcoal-button') || document.querySelector('.charcoal-pagination-button')) {
return true;
}
await sleep(500);
}
return false;
}
async function unfollowOne(btn) {
const beforeCount = getAllFollowButtons().length;
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(700);
try {
btn.click();
} catch (e) {
clickLikeHuman(btn);
}
await sleep(CONFIG.modalWait);
let confirmBtn = findConfirmButton();
if (confirmBtn) {
try {
confirmBtn.click();
} catch (e) {
clickLikeHuman(confirmBtn);
}
await sleep(CONFIG.confirmWait);
}
let afterCount = getAllFollowButtons().length;
if (afterCount < beforeCount) return true;
try {
clickLikeHuman(btn);
} catch (e) {}
await sleep(CONFIG.modalWait);
confirmBtn = findConfirmButton();
if (confirmBtn) {
try {
confirmBtn.click();
} catch (e) {
clickLikeHuman(confirmBtn);
}
await sleep(CONFIG.confirmWait);
}
afterCount = getAllFollowButtons().length;
return afterCount < beforeCount;
}
function createPanel() {
if (document.getElementById('__pixiv_unfollow_panel__')) return;
const panel = document.createElement('div');
panel.id = '__pixiv_unfollow_panel__';
panel.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
z-index: 999999;
width: 260px;
background: rgba(20,20,20,0.92);
color: #fff;
border-radius: 12px;
padding: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,.3);
font-size: 13px;
line-height: 1.5;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
`;
panel.innerHTML = `
<div style="font-weight:700;margin-bottom:8px;">Pixiv 自动取消关注</div>
<div id="__pixiv_unfollow_status__">初始化中...</div>
<div style="margin-top:10px;display:flex;gap:8px;">
<button id="__pixiv_unfollow_stop__" style="flex:1;padding:6px 8px;border:none;border-radius:8px;background:#ff5f57;color:#fff;cursor:pointer;">停止</button>
<button id="__pixiv_unfollow_hide__" style="flex:1;padding:6px 8px;border:none;border-radius:8px;background:#444;color:#fff;cursor:pointer;">隐藏</button>
</div>
`;
document.body.appendChild(panel);
document.getElementById('__pixiv_unfollow_stop__').onclick = () => {
clearState();
alert('已停止自动取消关注');
};
document.getElementById('__pixiv_unfollow_hide__').onclick = () => {
panel.style.display = 'none';
};
}
function renderPanel(state) {
createPanel();
const el = document.getElementById('__pixiv_unfollow_status__');
if (!el) return;
const page = getCurrentPageNumber();
el.innerHTML = `
<div>状态:${state.running ? '运行中' : '已停止'}</div>
<div>当前页:${page ?? '?'}</div>
<div>总计取消:${state.total}</div>
<div>本页取消:${state.pageDone}</div>
`;
}
async function main() {
const state = getState();
if (!state.running) return;
createPanel();
renderPanel(state);
const ready = await waitForPageReady();
if (!ready) {
console.warn('页面元素未准备好,停止运行');
return;
}
await sleep(CONFIG.startDelay);
let currentPage = getCurrentPageNumber();
if (!currentPage) {
console.warn('未识别到当前页码');
}
setState({
pageDone: 0,
lastPage: currentPage
});
console.log(`开始处理第 ${currentPage} 页`);
let failStreak = 0;
while (true) {
const currentState = loadState();
if (!currentState || !currentState.running) {
console.log('检测到停止指令,退出');
return;
}
const buttons = getFollowButtons();
if (!buttons.length) {
console.log(`第 ${currentPage} 页没有更多“已关注”按钮`);
break;
}
const ok = await unfollowOne(buttons[0]);
if (ok) {
const s = getState();
setState({
total: s.total + 1,
pageDone: s.pageDone + 1
});
failStreak = 0;
} else {
failStreak++;
console.warn(`取消失败 ${failStreak} 次`);
if (failStreak >= CONFIG.failLimit) {
console.warn('连续失败过多,结束本页');
break;
}
}
await sleep(rand(CONFIG.betweenClicks));
}
const nextLink = getNextPageLink();
if (!nextLink) {
console.log('没有下一页,任务结束');
const finalState = getState();
setState({ running: false });
alert(`自动取消关注完成,共取消 ${finalState.total} 个`);
clearState();
return;
}
console.log(`跳转下一页:${nextLink.href}`);
await sleep(CONFIG.betweenPages);
location.href = nextLink.href;
}
main();
})();bilibili 油猴版
// ==UserScript==
// @name Bilibili 批量取关(接口版)
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 通过接口自动分页获取关注列表并批量取关,不依赖页面按钮点击
// @match https://space.bilibili.com/*
// @match https://www.bilibili.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
pageSize: 50, // 每页拉取多少关注
minDelay: 2500, // 每次取关最小间隔
maxDelay: 5000, // 每次取关最大间隔
batchPauseEvery: 20, // 每处理多少个暂停一次
batchPauseTime: 30000, // 暂停时长
requestTimeout: 15000, // 请求超时
debug: true
};
const state = {
running: false,
stopped: false,
totalFetched: 0,
totalSuccess: 0,
totalFailed: 0,
currentPage: 1,
users: []
};
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
function log(...args) {
console.log('[BiliAPIUnfollow]', ...args);
const el = document.querySelector('#bili-api-unfollow-status');
if (el) el.textContent = args.join(' ');
}
function updateCounter() {
const el = document.querySelector('#bili-api-unfollow-counter');
if (el) {
el.innerHTML = `
已获取: ${state.totalFetched}<br>
成功: ${state.totalSuccess}<br>
失败: ${state.totalFailed}<br>
当前页: ${state.currentPage}
`;
}
}
function getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&') + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
}
function getCsrf() {
return getCookie('bili_jct');
}
function getUid() {
return getCookie('DedeUserID');
}
async function fetchWithTimeout(url, options = {}, timeout = CONFIG.requestTimeout) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, {
...options,
credentials: 'include',
signal: controller.signal
});
clearTimeout(id);
return res;
} catch (err) {
clearTimeout(id);
throw err;
}
}
async function getFollowings(page) {
const uid = getUid();
if (!uid) throw new Error('未获取到 DedeUserID,请确认已登录');
const url = `https://api.bilibili.com/x/relation/followings?vmid=${uid}&pn=${page}&ps=${CONFIG.pageSize}&order=desc&jsonp=jsonp`;
const res = await fetchWithTimeout(url, {
method: 'GET'
});
const data = await res.json();
if (data.code !== 0) {
throw new Error(`获取关注列表失败: code=${data.code}, message=${data.message}`);
}
return data?.data?.list || [];
}
async function unfollowUser(mid) {
const csrf = getCsrf();
if (!csrf) throw new Error('未获取到 bili_jct(csrf),请确认已登录');
const body = new URLSearchParams({
fid: String(mid),
act: '2',
re_src: '11',
csrf: csrf
});
const res = await fetchWithTimeout('https://api.bilibili.com/x/relation/modify', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: body.toString()
});
const data = await res.json();
return data;
}
async function fetchAllUsers() {
state.users = [];
state.currentPage = 1;
state.totalFetched = 0;
updateCounter();
while (state.running && !state.stopped) {
log(`正在读取第 ${state.currentPage} 页关注列表...`);
const list = await getFollowings(state.currentPage);
if (!list.length) {
log('关注列表读取完成');
break;
}
state.users.push(...list);
state.totalFetched = state.users.length;
updateCounter();
log(`第 ${state.currentPage} 页读取到 ${list.length} 个,累计 ${state.totalFetched} 个`);
state.currentPage++;
await sleep(1200);
}
}
async function runUnfollow() {
if (!state.users.length) {
log('没有获取到任何关注用户');
return;
}
log(`准备开始取关,共 ${state.users.length} 个`);
for (let i = 0; i < state.users.length; i++) {
if (!state.running || state.stopped) break;
const user = state.users[i];
const uname = user.uname || user.name || `mid:${user.mid}`;
try {
log(`正在取关 [${i + 1}/${state.users.length}] ${uname}`);
const result = await unfollowUser(user.mid);
if (result.code === 0) {
state.totalSuccess++;
log(`取关成功: ${uname}`);
} else {
state.totalFailed++;
log(`取关失败: ${uname} code=${result.code} msg=${result.message || ''}`);
}
} catch (err) {
state.totalFailed++;
log(`取关异常: ${uname} ${err.message || err}`);
}
updateCounter();
if (state.totalSuccess > 0 && state.totalSuccess % CONFIG.batchPauseEvery === 0) {
log(`已成功取关 ${state.totalSuccess} 个,暂停 ${CONFIG.batchPauseTime / 1000} 秒`);
await sleep(CONFIG.batchPauseTime);
} else {
await sleep(rand(CONFIG.minDelay, CONFIG.maxDelay));
}
}
log(`任务结束。成功 ${state.totalSuccess},失败 ${state.totalFailed}`);
}
async function startTask() {
if (state.running) {
alert('任务已经在运行中');
return;
}
const csrf = getCsrf();
const uid = getUid();
if (!csrf || !uid) {
alert('未检测到登录态,请先登录 Bilibili');
return;
}
state.running = true;
state.stopped = false;
state.totalFetched = 0;
state.totalSuccess = 0;
state.totalFailed = 0;
state.currentPage = 1;
state.users = [];
updateCounter();
try {
await fetchAllUsers();
if (!state.stopped) {
await runUnfollow();
}
} catch (err) {
console.error(err);
log(`运行失败: ${err.message || err}`);
}
state.running = false;
if (state.stopped) {
log('任务已停止');
}
}
function stopTask() {
state.stopped = true;
state.running = false;
log('正在停止任务...');
}
function createPanel() {
if (document.querySelector('#bili-api-unfollow-panel')) return;
const panel = document.createElement('div');
panel.id = 'bili-api-unfollow-panel';
panel.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
z-index: 999999;
width: 270px;
background: rgba(24,24,24,.95);
color: #fff;
border-radius: 10px;
padding: 14px;
box-shadow: 0 4px 16px rgba(0,0,0,.3);
font-size: 14px;
line-height: 1.5;
`;
panel.innerHTML = `
<div style="font-size:16px;font-weight:bold;margin-bottom:8px;">B站批量取关(接口版)</div>
<div id="bili-api-unfollow-counter" style="margin-bottom:8px;">
已获取: 0<br>
成功: 0<br>
失败: 0<br>
当前页: 1
</div>
<div id="bili-api-unfollow-status" style="margin-bottom:10px;color:#ddd;font-size:12px;">等待开始</div>
<div style="display:flex;gap:8px;">
<button id="bili-api-unfollow-start" style="flex:1;padding:8px;border:none;border-radius:6px;background:#00aeec;color:#fff;cursor:pointer;">开始</button>
<button id="bili-api-unfollow-stop" style="flex:1;padding:8px;border:none;border-radius:6px;background:#fb7299;color:#fff;cursor:pointer;">停止</button>
</div>
<div style="margin-top:8px;font-size:12px;color:#bbb;">建议低速运行,避免风控</div>
`;
document.body.appendChild(panel);
document.querySelector('#bili-api-unfollow-start').addEventListener('click', startTask);
document.querySelector('#bili-api-unfollow-stop').addEventListener('click', stopTask);
}
window.addEventListener('load', () => {
setTimeout(createPanel, 1500);
});
})();删除bilibili粉丝团勋章,有小问题但是能用
// ==UserScript==
// @name B站粉丝勋章自动逐个删除-刷新/局部更新兼容版
// @namespace https://tampermonkey.net/
// @version 2.2.0
// @description 自动逐个删除粉丝勋章,兼容页面刷新和局部更新两种模式
// @author ChatGPT
// @match *://link.bilibili.com/p/*
// @match *://link.bilibili.com/*
// @match *://live.bilibili.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
debug: true,
autoStart: true,
pageReadyDelay: 3000,
afterDeleteClickDelay: 1500,
dialogAppearTimeout: 15000,
afterConfirmDelay: 800,
waitDeleteResultTimeout: 20000,
resultCheckInterval: 800,
maxDeleteCount: 500,
runKey: 'bili_medal_auto_delete_running',
stopKey: 'bili_medal_auto_delete_stop',
countKey: 'bili_medal_auto_delete_count',
lockKey: 'bili_medal_auto_delete_lock'
};
let pageUnloading = false;
window.addEventListener('beforeunload', () => {
pageUnloading = true;
});
function log(...args) {
if (CONFIG.debug) console.log('[勋章自动删除]', ...args);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function text(el) {
return (el?.innerText || el?.textContent || '').replace(/\s+/g, ' ').trim();
}
function isVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
rect.width > 0 &&
rect.height > 0;
}
function fireMouse(el, type, x, y) {
el.dispatchEvent(new MouseEvent(type, {
view: window,
bubbles: true,
cancelable: true,
clientX: x,
clientY: y
}));
}
function strongClick(el) {
if (!el) return false;
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
['pointerover', 'mouseover', 'mouseenter', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']
.forEach(type => fireMouse(el, type, x, y));
if (typeof el.click === 'function') el.click();
return true;
}
function getRows() {
return Array.from(document.querySelectorAll('tbody tr'))
.filter(row => row.querySelector('.fans-medal-item'));
}
function getRowName(row) {
if (!row) return '';
const medal = row.querySelector('.fans-medal-content');
const anchor = row.querySelector('a[title]');
return `${text(medal)} / ${anchor?.getAttribute('title') || text(anchor) || ''}`.trim();
}
function getFirstRow() {
const rows = getRows();
return rows.length ? rows[0] : null;
}
function getFirstRowName() {
return getRowName(getFirstRow());
}
function findDeleteBtnInRow(row) {
if (!row) return null;
const lastTd = row.querySelector('td:last-child');
if (!lastTd) return null;
const spans = Array.from(lastTd.querySelectorAll('span'));
return spans.find(el => text(el) === '删除') || null;
}
function clickDeleteInRow(row) {
const btn = findDeleteBtnInRow(row);
if (!btn) return false;
log('命中删除按钮:', btn.outerHTML);
strongClick(btn);
return true;
}
function getVisibleDialogs() {
return Array.from(document.querySelectorAll('.link-popup-panel')).filter(isVisible);
}
function findNoticeDialog() {
const dialogs = getVisibleDialogs();
let dialog = dialogs.find(d => {
const t = text(d);
return t.includes('删除粉丝勋章须知') || t.includes('确认删除');
});
if (dialog) return dialog;
dialog = dialogs.find(d => Array.from(d.querySelectorAll('button')).some(b => text(b).includes('确认删除')));
if (dialog) return dialog;
if (dialogs.length === 1) return dialogs[0];
return null;
}
function findConfirmDeleteBtn(dialog) {
if (!dialog) return null;
const buttons = Array.from(dialog.querySelectorAll('button')).filter(isVisible);
return buttons.find(btn => text(btn).includes('确认删除')) || null;
}
async function waitFor(fn, timeout = 5000, interval = 250) {
const start = Date.now();
while (Date.now() - start < timeout) {
const result = fn();
if (result) return result;
await sleep(interval);
}
return null;
}
function forceEnableButton(btn) {
if (!btn) return false;
btn.disabled = false;
btn.removeAttribute('disabled');
btn.setAttribute('aria-disabled', 'false');
btn.classList.remove('disabled');
return true;
}
async function processNoticeDialog() {
const dialog = await waitFor(() => findNoticeDialog(), CONFIG.dialogAppearTimeout, 300);
if (!dialog) {
log('失败:没找到须知弹窗');
return false;
}
log('找到须知弹窗');
const confirmBtn = findConfirmDeleteBtn(dialog);
if (!confirmBtn) {
log('失败:没找到确认删除按钮');
return false;
}
forceEnableButton(confirmBtn);
await sleep(300);
strongClick(confirmBtn);
await sleep(CONFIG.afterConfirmDelay);
return true;
}
function setRunning(v) {
localStorage.setItem(CONFIG.runKey, v ? '1' : '0');
}
function isRunning() {
return localStorage.getItem(CONFIG.runKey) === '1';
}
function setStop(v) {
localStorage.setItem(CONFIG.stopKey, v ? '1' : '0');
}
function isStopped() {
return localStorage.getItem(CONFIG.stopKey) === '1';
}
function getDeleteCount() {
return Number(localStorage.getItem(CONFIG.countKey) || '0');
}
function setDeleteCount(n) {
localStorage.setItem(CONFIG.countKey, String(n));
}
function resetState() {
setDeleteCount(0);
setStop(false);
localStorage.removeItem(CONFIG.lockKey);
}
function acquireLock() {
if (localStorage.getItem(CONFIG.lockKey) === '1') return false;
localStorage.setItem(CONFIG.lockKey, '1');
return true;
}
function releaseLock() {
localStorage.removeItem(CONFIG.lockKey);
}
async function waitTableReady(timeout = 20000) {
const start = Date.now();
while (Date.now() - start < timeout) {
if (document.querySelector('tbody')) return true;
await sleep(500);
}
return false;
}
async function waitDeleteResult(beforeName, beforeCount, timeout = CONFIG.waitDeleteResultTimeout) {
const start = Date.now();
while (Date.now() - start < timeout) {
if (pageUnloading) {
log('检测到页面即将刷新');
return 'reload';
}
const rows = getRows();
const currentCount = rows.length;
const currentFirstName = rows.length ? getRowName(rows[0]) : '';
log('等待删除结果中:', {
beforeName,
currentFirstName,
beforeCount,
currentCount
});
if (currentCount === 0) {
log('列表已空');
return 'empty';
}
if (currentCount < beforeCount) {
log('检测到行数减少');
return 'count_changed';
}
if (currentFirstName && currentFirstName !== beforeName) {
log('检测到第一项已变化');
return 'first_changed';
}
await sleep(CONFIG.resultCheckInterval);
}
return null;
}
async function deleteOne() {
const rows = getRows();
if (!rows.length) {
setRunning(false);
alert(`粉丝勋章已删除完成,共删除 ${getDeleteCount()} 个。`);
return 'done';
}
const firstRow = rows[0];
const beforeCount = rows.length;
const beforeName = getRowName(firstRow);
log('准备删除第一个勋章:', beforeName);
const clicked = clickDeleteInRow(firstRow);
if (!clicked) {
setRunning(false);
alert('删除失败:没找到第一项的删除按钮。');
return 'fail';
}
await sleep(CONFIG.afterDeleteClickDelay);
const ok = await processNoticeDialog();
if (!ok) {
setRunning(false);
alert('删除失败:没找到确认弹窗或确认按钮。');
return 'fail';
}
setDeleteCount(getDeleteCount() + 1);
log(`已提交第 ${getDeleteCount()} 次删除`);
const result = await waitDeleteResult(beforeName, beforeCount);
if (!result) {
setRunning(false);
alert('删除后既没有刷新,也没有检测到列表变化,脚本已停止。');
return 'fail';
}
return result;
}
async function runLoop() {
while (isRunning() && !isStopped()) {
if (getDeleteCount() >= CONFIG.maxDeleteCount) {
setRunning(false);
alert(`已达到最大保护次数 ${CONFIG.maxDeleteCount},脚本已停止。`);
return;
}
const rows = getRows();
if (!rows.length) {
setRunning(false);
alert(`粉丝勋章已删除完成,共删除 ${getDeleteCount()} 个。`);
return;
}
const result = await deleteOne();
if (result === 'reload') {
log('等待页面刷新后续跑');
return;
}
if (result === 'done') {
return;
}
if (result === 'fail') {
return;
}
log('当前页继续下一轮删除,结果类型:', result);
await sleep(1500);
}
}
function createButtons() {
if (!document.getElementById('tm-bili-medal-auto-delete-start')) {
const startBtn = document.createElement('button');
startBtn.id = 'tm-bili-medal-auto-delete-start';
startBtn.textContent = '开始自动删除';
Object.assign(startBtn.style, {
position: 'fixed',
right: '20px',
bottom: '120px',
zIndex: '999999',
padding: '10px 14px',
background: '#e74c3c',
color: '#fff',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '14px',
boxShadow: '0 2px 8px rgba(0,0,0,.25)'
});
startBtn.onclick = async () => {
resetState();
setRunning(true);
await startRun();
};
document.body.appendChild(startBtn);
}
if (!document.getElementById('tm-bili-medal-auto-delete-stop')) {
const stopBtn = document.createElement('button');
stopBtn.id = 'tm-bili-medal-auto-delete-stop';
stopBtn.textContent = '停止自动删除';
Object.assign(stopBtn.style, {
position: 'fixed',
right: '20px',
bottom: '70px',
zIndex: '999999',
padding: '10px 14px',
background: '#555',
color: '#fff',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '14px',
boxShadow: '0 2px 8px rgba(0,0,0,.25)'
});
stopBtn.onclick = () => {
setStop(true);
setRunning(false);
releaseLock();
alert('已停止自动删除。');
};
document.body.appendChild(stopBtn);
}
}
async function startRun() {
if (!acquireLock()) {
log('已有一个删除流程在运行,跳过');
return;
}
try {
await waitTableReady();
await sleep(CONFIG.pageReadyDelay);
if (!isRunning() || isStopped()) return;
await runLoop();
} finally {
if (!pageUnloading) {
releaseLock();
}
}
}
window.addEventListener('load', async () => {
createButtons();
setInterval(createButtons, 2000);
if (CONFIG.autoStart && isRunning() && !isStopped()) {
log('检测到上次任务未完成,自动续跑');
await startRun();
}
});
})();