// ==UserScript==
// @name Bilibili 半自动关注清理助手 v0.6
// @namespace https://docs.scriptcat.org/
// @version 0.6.0
// @description B站关注页半自动清理:快速队列扫描当前页“已关注/已互粉”按钮,按队列快速点击;支持弹窗反馈、手动/自动刷新当前页继续。
// @author know634 + ChatGPT
// @match https://space.bilibili.com/*/relation/follow*
// @grant none
// @noframes
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/**
* Bilibili 半自动关注清理助手 v0.6
*
* v0.6 修正:
* 1. 保留快速队列模式:一次扫描当前页目标按钮,灌入快照队列。
* 2. 目标状态收敛为“已关注 / 已互粉”。
* 3. 删除“特别关注”入口,避免误操作。
*
* 设计边界:
* 1. 只模拟用户在当前页面点击,不调用 B 站内部关注接口。
* 2. 不读取 cookie/token,不绕过验证码/风控/登录限制。
* 3. 自动刷新模式只刷新当前 URL,不自动翻页、不切换分组、不修改 tagid。
* 4. 每次只处理一种关注状态:已关注 / 已互粉。
*/
const STORAGE_KEY = 'bilibili_unfollow_helper_v0_6_state';
const LEGACY_STORAGE_KEYS = [
'bilibili_unfollow_helper_v0_5_state',
'bilibili_unfollow_helper_v0_5_1_state',
'bilibili_unfollow_helper_v0_5_2_state',
];
const PANEL_ID = 'tm-bili-unfollow-helper-v06';
const FLOAT_ID = 'tm-bili-unfollow-float-v06';
const MODAL_ID = 'tm-bili-unfollow-modal-v06';
const TARGETS = {
NORMAL: '已关注',
MUTUAL: '已互粉',
};
const ALLOWED_TARGET_TEXTS = new Set(Object.values(TARGETS));
const runtime = {
running: false,
stopRequested: false,
activeTarget: '',
currentRoundDone: 0,
currentRoundFailed: 0,
statusTimer: null,
};
const defaultConfig = {
autoRefresh: false,
maxTotal: 100,
batchMax: 50,
minDelay: 120,
maxDelay: 260,
verifyDelay: 0,
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function clampNumber(value, fallback, min, max) {
const n = Number(value);
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, n));
}
function randomDelay(min, max) {
const lo = Math.min(min, max);
const hi = Math.max(min, max);
return Math.floor(lo + Math.random() * Math.max(0, hi - lo));
}
function normalizeText(el) {
return (el?.innerText || el?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
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' &&
style.opacity !== '0'
);
}
function safeJsonParse(text, fallback) {
try {
return JSON.parse(text);
} catch (_) {
return fallback;
}
}
function readState() {
const state = safeJsonParse(localStorage.getItem(STORAGE_KEY), null);
if (!state) return null;
if (state.targetText && !ALLOWED_TARGET_TEXTS.has(state.targetText)) {
clearState();
return null;
}
return state;
}
function writeState(state) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function clearState() {
localStorage.removeItem(STORAGE_KEY);
}
function clearLegacyStates() {
for (const key of LEGACY_STORAGE_KEYS) {
localStorage.removeItem(key);
}
}
function getConfigFromPanel() {
return {
autoRefresh: Boolean(document.querySelector('#tm-bili-auto-refresh')?.checked),
maxTotal: clampNumber(document.querySelector('#tm-bili-max-total')?.value, defaultConfig.maxTotal, 1, 9999),
batchMax: clampNumber(document.querySelector('#tm-bili-batch-max')?.value, defaultConfig.batchMax, 1, 1000),
minDelay: clampNumber(document.querySelector('#tm-bili-min-delay')?.value, defaultConfig.minDelay, 0, 60000),
maxDelay: clampNumber(document.querySelector('#tm-bili-max-delay')?.value, defaultConfig.maxDelay, 0, 60000),
verifyDelay: defaultConfig.verifyDelay,
};
}
function setPanelConfig(config) {
const autoRefresh = document.querySelector('#tm-bili-auto-refresh');
const maxTotal = document.querySelector('#tm-bili-max-total');
const batchMax = document.querySelector('#tm-bili-batch-max');
const minDelay = document.querySelector('#tm-bili-min-delay');
const maxDelay = document.querySelector('#tm-bili-max-delay');
if (autoRefresh) autoRefresh.checked = Boolean(config.autoRefresh);
if (maxTotal) maxTotal.value = String(config.maxTotal ?? defaultConfig.maxTotal);
if (batchMax) batchMax.value = String(config.batchMax ?? defaultConfig.batchMax);
if (minDelay) minDelay.value = String(config.minDelay ?? defaultConfig.minDelay);
if (maxDelay) maxDelay.value = String(config.maxDelay ?? defaultConfig.maxDelay);
}
function ensureStyle() {
if (document.querySelector('#tm-bili-unfollow-style-v06')) return;
const style = document.createElement('style');
style.id = 'tm-bili-unfollow-style-v06';
style.textContent = `
#${PANEL_ID} {
box-sizing: border-box;
width: 100%;
margin: 12px 0;
padding: 12px;
border: 1px solid #e3e5e7;
border-radius: 10px;
background: #ffffff;
color: #18191c;
font-size: 13px;
line-height: 1.6;
font-family: Arial, "Microsoft YaHei", sans-serif;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
#${PANEL_ID}.tm-bili-fixed-fallback {
position: fixed;
right: 18px;
bottom: 18px;
width: 380px;
z-index: 999998;
}
#${PANEL_ID} * { box-sizing: border-box; }
#${PANEL_ID} .tm-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 8px;
}
#${PANEL_ID} .tm-title {
font-weight: 700;
font-size: 14px;
margin-bottom: 4px;
}
#${PANEL_ID} .tm-desc {
color: #61666d;
font-size: 12px;
}
#${PANEL_ID} button {
border: 1px solid #c9ccd0;
border-radius: 6px;
background: #fff;
color: #18191c;
padding: 6px 10px;
cursor: pointer;
font-size: 13px;
}
#${PANEL_ID} button:hover { background: #f6f7f8; }
#${PANEL_ID} button.tm-primary {
border-color: #00aeec;
background: #00aeec;
color: #ffffff;
}
#${PANEL_ID} button.tm-warning {
border-color: #ff7f24;
color: #d35f00;
}
#${PANEL_ID} button.tm-danger {
border-color: #f85a54;
color: #d93832;
}
#${PANEL_ID} button.tm-stop {
border-color: #f85a54;
background: #fff0ef;
color: #d93832;
}
#${PANEL_ID} input[type="number"] {
width: 76px;
padding: 4px 6px;
border: 1px solid #c9ccd0;
border-radius: 5px;
}
#${PANEL_ID} label {
display: inline-flex;
align-items: center;
gap: 4px;
color: #61666d;
font-size: 12px;
}
#${PANEL_ID} .tm-status-line {
margin-top: 8px;
padding: 8px;
border-radius: 6px;
background: #f6f7f8;
color: #61666d;
white-space: pre-wrap;
max-height: 88px;
overflow: auto;
}
#${FLOAT_ID} {
position: fixed;
left: 50%;
top: 90px;
transform: translateX(-50%);
min-width: 340px;
max-width: 620px;
z-index: 999999;
padding: 14px 18px;
border-radius: 10px;
background: rgba(24, 25, 28, 0.92);
color: #fff;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
font-size: 14px;
line-height: 1.7;
white-space: pre-wrap;
font-family: Arial, "Microsoft YaHei", sans-serif;
}
#${MODAL_ID} {
position: fixed;
inset: 0;
z-index: 1000000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.35);
font-family: Arial, "Microsoft YaHei", sans-serif;
}
#${MODAL_ID} .tm-modal-card {
width: min(560px, calc(100vw - 48px));
border-radius: 12px;
background: #fff;
color: #18191c;
box-shadow: 0 12px 38px rgba(0, 0, 0, 0.25);
overflow: hidden;
}
#${MODAL_ID} .tm-modal-title {
padding: 16px 18px 8px 18px;
font-size: 16px;
font-weight: 700;
}
#${MODAL_ID} .tm-modal-body {
padding: 4px 18px 14px 18px;
color: #61666d;
line-height: 1.7;
white-space: pre-wrap;
font-size: 14px;
}
#${MODAL_ID} .tm-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 18px 16px 18px;
border-top: 1px solid #f1f2f3;
}
#${MODAL_ID} button {
border: 1px solid #c9ccd0;
border-radius: 6px;
background: #fff;
color: #18191c;
padding: 7px 14px;
cursor: pointer;
}
#${MODAL_ID} button.tm-ok {
border-color: #00aeec;
background: #00aeec;
color: #fff;
}
#${MODAL_ID} button.tm-danger-ok {
border-color: #f85a54;
background: #f85a54;
color: #fff;
}
`;
document.head.appendChild(style);
}
function setStatusLine(text) {
const el = document.querySelector('#tm-bili-status-line');
if (el) el.textContent = text;
}
function showFloat(message, autoCloseMs = 0) {
let float = document.querySelector(`#${FLOAT_ID}`);
if (!float) {
float = document.createElement('div');
float.id = FLOAT_ID;
document.body.appendChild(float);
}
float.textContent = message;
float.style.display = 'block';
setStatusLine(message);
if (runtime.statusTimer) {
clearTimeout(runtime.statusTimer);
runtime.statusTimer = null;
}
if (autoCloseMs > 0) {
runtime.statusTimer = setTimeout(() => {
const f = document.querySelector(`#${FLOAT_ID}`);
if (f) f.style.display = 'none';
}, autoCloseMs);
}
}
function showModal({ title = '确认操作', message = '', okText = '确定', cancelText = '取消', danger = false, showCancel = true }) {
return new Promise(resolve => {
const old = document.querySelector(`#${MODAL_ID}`);
if (old) old.remove();
const modal = document.createElement('div');
modal.id = MODAL_ID;
modal.innerHTML = `
`;
modal.querySelector('.tm-modal-title').textContent = title;
modal.querySelector('.tm-modal-body').textContent = message;
const ok = modal.querySelector('.tm-ok');
ok.textContent = okText;
if (danger) ok.classList.add('tm-danger-ok');
const cancel = modal.querySelector('.tm-cancel');
if (cancel) cancel.textContent = cancelText;
ok.addEventListener('click', () => {
modal.remove();
resolve(true);
});
if (cancel) {
cancel.addEventListener('click', () => {
modal.remove();
resolve(false);
});
}
document.body.appendChild(modal);
});
}
function findVisibleTextElement(textList, selector = 'button, div, span, li, a, p, h1, h2, h3, [role="button"]') {
const candidates = Array.from(document.querySelectorAll(selector)).filter(isVisible);
return candidates.find(el => {
const t = normalizeText(el);
return textList.some(text => t === text || t.includes(text));
});
}
function getTargetFollowButtons(targetText) {
return Array.from(document.querySelectorAll('.follow-btn__trigger'))
.filter(isVisible)
.filter(el => normalizeText(el) === targetText);
}
function getAnyFollowTriggers() {
return Array.from(document.querySelectorAll('.follow-btn__trigger')).filter(isVisible);
}
function hasRiskOrLoginGate() {
const riskTexts = [
'安全验证',
'请完成验证',
'验证码',
'登录失效',
'请先登录',
'操作频繁',
'风控',
];
return Boolean(findVisibleTextElement(riskTexts));
}
function simulateClick(el) {
if (!el) return;
const opts = { bubbles: true, cancelable: true, view: window };
el.dispatchEvent(new MouseEvent('mouseover', opts));
el.dispatchEvent(new MouseEvent('mousedown', opts));
el.dispatchEvent(new MouseEvent('mouseup', opts));
el.dispatchEvent(new MouseEvent('click', opts));
}
function getCardBrief(btn) {
const card = btn.closest('.relation-card') ||
btn.closest('[class*="relation-card"]') ||
btn.closest('li') ||
btn.closest('[class*="card"]');
const text = normalizeText(card || btn);
return text.slice(0, 80) || '当前账号';
}
async function waitForPageContent(timeout = 12000) {
const start = Date.now();
while (Date.now() - start < timeout) {
if (getAnyFollowTriggers().length > 0) return true;
if (hasRiskOrLoginGate()) return true;
await sleep(250);
}
return false;
}
async function verifyButtonChanged(btn, targetText, beforeCount, verifyDelay) {
await sleep(verifyDelay);
const afterCount = getTargetFollowButtons(targetText).length;
if (!btn.isConnected) {
return { ok: true, reason: '原按钮节点已被页面重绘/移除' };
}
if (!isVisible(btn)) {
return { ok: true, reason: '原按钮已不可见' };
}
const afterText = normalizeText(btn);
if (afterText !== targetText) {
return { ok: true, reason: `按钮文字已变化:${afterText || '空'}` };
}
if (afterCount < beforeCount) {
return { ok: true, reason: `目标按钮数量减少:${beforeCount} → ${afterCount}` };
}
return {
ok: false,
reason: `点击后仍看到相同状态【${targetText}】,目标按钮数 ${beforeCount} → ${afterCount}`,
};
}
function collectTargetButtonSnapshot(targetText, limit) {
const seen = new Set();
const queue = [];
for (const el of Array.from(document.querySelectorAll('.follow-btn__trigger'))) {
if (queue.length >= limit) break;
if (!el || seen.has(el)) continue;
seen.add(el);
if (!isVisible(el)) continue;
if (normalizeText(el) !== targetText) continue;
queue.push(el);
}
return queue;
}
async function clickSnapshotQueue(targetText, state) {
if (hasRiskOrLoginGate()) {
throw new Error('检测到登录/验证/风控相关提示,已停止。');
}
const remainingTotal = Math.max(0, state.maxTotal - state.doneTotal);
const limit = Math.min(state.batchMax, remainingTotal);
const queue = collectTargetButtonSnapshot(targetText, limit);
if (queue.length === 0) {
return { type: 'no-target', queued: 0, clicked: 0, skipped: 0, failed: 0 };
}
showFloat(
`已扫描当前页【${targetText}】按钮:${queue.length} 个
` +
`进入快速队列模式:不再每点一次重扫整页。
` +
`随机间隔:${state.minDelay} ~ ${state.maxDelay} ms
` +
`可随时点击“停止自动清理”。`
);
let clicked = 0;
let skipped = 0;
let failed = 0;
for (let i = 0; i < queue.length; i += 1) {
if (runtime.stopRequested) break;
if (hasRiskOrLoginGate()) {
throw new Error('检测到登录/验证/风控相关提示,已停止。');
}
const btn = queue[i];
const indexText = `${i + 1} / ${queue.length}`;
try {
if (!btn || !btn.isConnected) {
skipped += 1;
continue;
}
if (!isVisible(btn)) {
skipped += 1;
continue;
}
const currentText = normalizeText(btn);
if (currentText !== targetText) {
skipped += 1;
continue;
}
const brief = getCardBrief(btn);
simulateClick(btn);
clicked += 1;
runtime.currentRoundDone += 1;
state.doneTotal += 1;
state.failureStreak = 0;
if (clicked === 1 || clicked % 5 === 0 || i === queue.length - 1) {
writeState(state);
showFloat(
`快速点击中:【${targetText}】
` +
`队列进度:${indexText}
` +
`本轮已点:${clicked}
` +
`总计:${state.doneTotal} / ${state.maxTotal}
` +
`当前:${brief}`,
0
);
}
const delay = randomDelay(state.minDelay, state.maxDelay);
if (delay > 0) await sleep(delay);
} catch (err) {
failed += 1;
state.failTotal += 1;
state.failureStreak += 1;
writeState(state);
showFloat(
`快速点击异常:${err?.message || err}
` +
`队列进度:${indexText}
` +
`连续失败:${state.failureStreak} / 3`,
1800
);
if (state.failureStreak >= 3) break;
}
}
writeState(state);
return {
type: clicked > 0 ? 'success' : 'failed',
queued: queue.length,
clicked,
skipped,
failed,
};
}
function stopAll(reason = '已停止。', clear = true) {
runtime.running = false;
runtime.stopRequested = true;
if (clear) clearState();
showFloat(reason, 5000);
updateButtonsRunning(false);
}
function updateButtonsRunning(isRunning) {
const buttons = document.querySelectorAll(`#${PANEL_ID} button[data-target]`);
buttons.forEach(btn => {
btn.disabled = isRunning;
btn.style.opacity = isRunning ? '0.55' : '1';
});
const stopBtn = document.querySelector('#tm-bili-stop');
if (stopBtn) {
stopBtn.disabled = !isRunning && !readState()?.active;
stopBtn.style.opacity = stopBtn.disabled ? '0.55' : '1';
}
}
async function runBatch(state) {
if (runtime.running) return;
runtime.running = true;
runtime.stopRequested = false;
runtime.activeTarget = state.targetText;
runtime.currentRoundDone = 0;
runtime.currentRoundFailed = 0;
updateButtonsRunning(true);
try {
await waitForPageContent();
if (state.doneTotal >= state.maxTotal) {
stopAll(`已达到最大总数:${state.doneTotal} / ${state.maxTotal}。`, true);
return;
}
const result = await clickSnapshotQueue(state.targetText, state);
if (result.type === 'no-target') {
runtime.running = false;
updateButtonsRunning(false);
clearState();
showFloat(`当前页没有可处理的【${state.targetText}】按钮,清理结束。`, 8000);
return;
}
runtime.currentRoundFailed = (result.skipped || 0) + (result.failed || 0);
if (result.type === 'failed' && result.clicked === 0) {
state.failureStreak += 1;
writeState(state);
if (state.failureStreak >= 3) {
stopAll('连续失败 3 次,已停止。建议手动检查页面按钮是否仍能直接点击取消。', true);
return;
}
}
runtime.running = false;
updateButtonsRunning(false);
const summary =
`本轮快速队列完成。
` +
`目标状态:【${state.targetText}】
` +
`本轮扫描:${result.queued}
` +
`本轮点击:${result.clicked}
` +
`跳过/失效:${result.skipped}
` +
`异常:${result.failed}
` +
`总计点击:${state.doneTotal} / ${state.maxTotal}`;
if (runtime.stopRequested) {
clearState();
showFloat(`已停止。
${summary}`, 5000);
return;
}
if (state.doneTotal >= state.maxTotal) {
clearState();
showFloat(`已达到最大总数,自动结束。
${summary}`, 8000);
return;
}
if (state.autoRefresh) {
if (result.clicked > 0) {
state.round += 1;
state.lastUrl = location.href;
state.active = true;
state.resumeAfterReload = true;
writeState(state);
showFloat(
`${summary}
自动刷新模式已开启。
1.5 秒后刷新当前页,继续处理补位到第一页的【${state.targetText}】。`,
0
);
await sleep(1500);
if (!runtime.stopRequested) {
location.reload();
}
return;
}
clearState();
showFloat(
`${summary}
当前页没有成功点击项,自动刷新清理结束。`,
8000
);
return;
}
clearState();
showFloat(
`${summary}
当前为手动刷新模式。你可以手动刷新页面或翻页后再点一次按钮。`,
8000
);
} catch (err) {
clearState();
runtime.running = false;
updateButtonsRunning(false);
showFloat(`异常停止:${err?.message || err}
建议手动检查页面状态。`, 10000);
}
}
async function startByTarget(targetText) {
if (!ALLOWED_TARGET_TEXTS.has(targetText)) {
showFloat(`不支持的目标状态:【${targetText}】,已拒绝执行。`, 5000);
return;
}
const config = getConfigFromPanel();
const danger = targetText !== TARGETS.NORMAL;
const message =
`即将直接点击当前页可见的【${targetText}】按钮本身。\n` +
`不会查找二级菜单“取消关注”。\n\n` +
`单轮最大:${config.batchMax}\n` +
`最大总数:${config.maxTotal}\n` +
`快速点击间隔:${config.minDelay} ~ ${config.maxDelay} ms\n` +
`刷新模式:${config.autoRefresh ? '自动刷新当前页并继续' : '手动刷新 / 手动翻页'}\n\n` +
`${targetText === TARGETS.NORMAL ? '推荐用于清理低价值单向关注。' : '已互粉可能包含熟人、同行或曾经互动对象,请确认后再继续。'}`;
const ok = await showModal({
title: `确认取消【${targetText}】`,
message,
okText: '开始处理',
cancelText: '取消',
danger,
showCancel: true,
});
if (!ok) {
showFloat('已取消本次操作。', 2500);
return;
}
const state = {
active: config.autoRefresh,
autoRefresh: config.autoRefresh,
targetText,
maxTotal: config.maxTotal,
batchMax: config.batchMax,
minDelay: config.minDelay,
maxDelay: config.maxDelay,
verifyDelay: config.verifyDelay,
doneTotal: 0,
failTotal: 0,
failureStreak: 0,
round: 1,
startedAt: Date.now(),
startedUrl: location.href,
lastUrl: location.href,
resumeAfterReload: false,
};
if (config.autoRefresh) {
writeState(state);
} else {
clearState();
}
runBatch(state);
}
function createPanel() {
if (document.querySelector(`#${PANEL_ID}`)) return;
ensureStyle();
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = `
关注清理助手 v0.6
快速队列模式:一次扫描当前页【已关注 / 已互粉】按钮,灌入列表后快速点击;自动刷新只刷新当前 URL,不翻页、不切分组。
脚本已加载。v0.6 为快速队列模式:先扫描本页【已关注 / 已互粉】按钮,再按队列快速点击。
`;
panel.querySelectorAll('button[data-target]').forEach(btn => {
btn.addEventListener('click', () => {
const target = btn.getAttribute('data-target');
startByTarget(target);
});
});
panel.querySelector('#tm-bili-stop').addEventListener('click', () => {
stopAll('已手动停止,并清除了自动刷新续跑状态。', true);
});
mountPanel(panel);
updateButtonsRunning(false);
}
function elementTextIncludes(el, text) {
return normalizeText(el).includes(text);
}
function findBatchOperationElement() {
const candidates = Array.from(document.querySelectorAll('button, div, span, a'))
.filter(isVisible)
.filter(el => elementTextIncludes(el, '批量操作'));
return candidates[0] || null;
}
function tryMountPanelUnderBatch(panel) {
const batchEl = findBatchOperationElement();
if (!batchEl) return false;
const host = batchEl.closest('div') || batchEl.parentElement;
const parent = host?.parentElement;
if (!parent) return false;
panel.classList.remove('tm-bili-fixed-fallback');
parent.insertBefore(panel, host.nextSibling);
panel.dataset.mountedUnderBatch = '1';
return true;
}
function mountPanel(panel) {
if (tryMountPanelUnderBatch(panel)) return;
const relationContainer = document.querySelector('[class*="relation"]') || document.querySelector('#app') || document.body;
if (relationContainer && relationContainer !== document.body) {
relationContainer.insertBefore(panel, relationContainer.firstChild);
return;
}
panel.classList.add('tm-bili-fixed-fallback');
document.body.appendChild(panel);
}
function relocatePanelIfBatchAppears() {
const panel = document.querySelector(`#${PANEL_ID}`);
if (!panel || panel.dataset.mountedUnderBatch === '1') return;
tryMountPanelUnderBatch(panel);
}
async function autoResumeIfNeeded() {
const state = readState();
if (!state || !state.active || !state.autoRefresh) return;
if (!ALLOWED_TARGET_TEXTS.has(state.targetText)) {
clearState();
showFloat('检测到旧版本无效续跑状态,已清除。', 5000);
return;
}
createPanel();
setPanelConfig(state);
updateButtonsRunning(true);
showFloat(
`检测到自动刷新续跑任务。\n` +
`目标状态:【${state.targetText}】\n` +
`已完成:${state.doneTotal} / ${state.maxTotal}\n` +
`轮次:${state.round}\n\n` +
`页面加载完成后继续。可点击“停止自动清理”。`,
0
);
await sleep(1500);
if (runtime.stopRequested) return;
if (state.lastUrl && state.lastUrl !== location.href) {
clearState();
showFloat('当前 URL 与自动刷新任务 URL 不一致,已停止续跑。', 8000);
updateButtonsRunning(false);
return;
}
runBatch(state);
}
function bootstrap() {
clearLegacyStates();
createPanel();
const state = readState();
if (state && state.autoRefresh) {
setPanelConfig(state);
}
autoResumeIfNeeded();
}
function waitDomReady() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
} else {
bootstrap();
}
}
waitDomReady();
// B 站是 Vue 动态页面;如果“批量操作”稍后才渲染,尝试补挂一次。
let retryCount = 0;
const mountRetry = setInterval(() => {
retryCount += 1;
if (!document.querySelector(`#${PANEL_ID}`)) {
createPanel();
} else {
relocatePanelIfBatchAppears();
}
const panel = document.querySelector(`#${PANEL_ID}`);
if (retryCount >= 20 || panel?.dataset.mountedUnderBatch === '1') {
clearInterval(mountRetry);
}
}, 500);
})();