// ==UserScript==
// @name Bilibili 半自动动态列表清理助手 v0.3
// @namespace https://chat.openai.com/know634
// @version 0.3.0
// @description 半自动清理 B 站空间动态:普通动态自动删,视频稿件联删默认取消并跳过;也可切换为危险:全部确认删除。
// @author know634 + ChatGPT
// @match https://space.bilibili.com/*/dynamic*
// @match https://www.bilibili.com/opus/*
// @icon https://www.bilibili.com/favicon.ico
// @grant none
// @run-at document-idle
// ==/UserScript==
(() => {
"use strict";
/**
* Bilibili 半自动动态列表清理助手 v0.3
*
* 设计口径:
* 1. 只模拟人工点击,不调用删除接口。
* 2. 删除候选始终取当前已加载列表中第一个“未跳过”的动态,不缓存待删队列。
* 3. 默认安全策略:普通动态自动确认;视频稿件联删弹窗自动取消并跳过该动态。
* 4. 强制策略:无论普通动态还是视频稿件联删,只要出现“确认删除”弹窗就确认。
* 5. 成功确认 DOM 变化后才计入 deleted;跳过只计入 skipped,不计入 deleted。
* 6. 删除数量和删除间隔的参数校验彻底拆开,避免毫秒参数被条数上限误伤。
*/
const APP_ID = "bili-dyn-cleaner-v030";
const PANEL_ID = `${APP_ID}-panel`;
const STYLE_ID = `${APP_ID}-style`;
const SELECTORS = {
item: ".bili-dyn-list__item",
moreButton: ".bili-dyn-more__btn",
menuLabel: ".bili-cascader-options__item-label",
menuItem: ".bili-cascader-options__item",
modalWrap: ".bili-modal__wrap",
modal: ".bili-modal",
modalTitle: ".bili-modal__title",
modalContent: ".bili-modal__content",
modalFooter: ".bili-modal__footer",
modalButton: "button.bili-modal__button",
confirmButton: "button.bili-modal__button.confirm.red",
cancelButton: "button.bili-modal__button.cancel",
};
const MODE = {
SAFE_SKIP_VIDEO: "safe_skip_video",
FORCE_CONFIRM_ALL: "force_confirm_all",
};
const MODAL_TYPE = {
NORMAL_DYNAMIC: "normal_dynamic",
VIDEO_ARCHIVE: "video_archive",
UNKNOWN_DELETE: "unknown_delete",
};
const DEFAULTS = {
maxCount: 50,
minDelayMs: 1000,
maxDelayMs: 1200,
mode: MODE.SAFE_SKIP_VIDEO,
};
const LIMITS = {
minDeleteCount: 1,
maxDeleteCount: 10000,
minDelayMs: 300,
recommendedMinDelayMs: 1000,
maxDelayMs: 60000,
lowCandidateThreshold: 2,
maxLoadMoreFailures: 3,
maxContinuousSkips: 200,
waitMenuMs: 3500,
waitModalMs: 3500,
waitModalGoneMs: 3500,
waitRemovedMs: 9000,
waitLoadMoreMs: 3500,
};
const state = {
running: false,
paused: false,
stopRequested: false,
target: 0,
deleted: 0,
skipped: 0,
failures: 0,
loadMoreFailures: 0,
continuousSkips: 0,
mode: DEFAULTS.mode,
status: "等待开始",
logs: [],
skippedFingerprints: new Set(),
};
const ui = {
panel: null,
countInput: null,
minDelayInput: null,
maxDelayInput: null,
modeSelect: null,
quick50Button: null,
quick100Button: null,
customButton: null,
pauseButton: null,
stopButton: null,
progressText: null,
skippedText: null,
statusText: null,
itemCountText: null,
modeText: null,
dangerTip: null,
logBox: null,
};
function init() {
if (document.getElementById(PANEL_ID)) return;
injectStyle();
createPanel();
updateUi();
log("v0.3 已加载:默认普通动态自动删;视频稿件联删自动取消并跳过。手动开始,不自动删除。");
}
function injectStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${PANEL_ID} {
position: fixed;
right: 24px;
top: 96px;
width: 310px;
z-index: 2147483647;
box-sizing: border-box;
padding: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.96);
color: #18191c;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
border: 1px solid rgba(0, 0, 0, 0.08);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 13px;
line-height: 1.45;
}
#${PANEL_ID} * { box-sizing: border-box; }
#${PANEL_ID} .bdc-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
font-weight: 700;
font-size: 15px;
}
#${PANEL_ID} .bdc-badge {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 7px;
border-radius: 999px;
background: #f1f2f3;
color: #61666d;
font-size: 12px;
font-weight: 500;
}
#${PANEL_ID} .bdc-row {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
#${PANEL_ID} .bdc-row label {
flex: 0 0 64px;
color: #61666d;
}
#${PANEL_ID} input, #${PANEL_ID} select {
height: 28px;
padding: 0 8px;
border: 1px solid #dcdfe6;
border-radius: 6px;
outline: none;
background: #fff;
color: #18191c;
}
#${PANEL_ID} input { width: 76px; }
#${PANEL_ID} select { flex: 1; min-width: 0; }
#${PANEL_ID} input:focus, #${PANEL_ID} select:focus {
border-color: #00aeec;
box-shadow: 0 0 0 2px rgba(0, 174, 236, 0.12);
}
#${PANEL_ID} button {
height: 30px;
padding: 0 10px;
border: 0;
border-radius: 7px;
cursor: pointer;
color: #fff;
background: #00aeec;
font-size: 13px;
transition: opacity .15s ease, transform .08s ease;
}
#${PANEL_ID} button:hover { opacity: 0.9; }
#${PANEL_ID} button:active { transform: translateY(1px); }
#${PANEL_ID} button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
#${PANEL_ID} .bdc-button-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 10px;
}
#${PANEL_ID} .bdc-button-row.single { grid-template-columns: 1fr; }
#${PANEL_ID} .bdc-control-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
#${PANEL_ID} .bdc-danger { background: #f85a54; }
#${PANEL_ID} .bdc-warn { background: #ff9f0a; }
#${PANEL_ID} .bdc-danger-tip {
display: none;
margin: 8px 0 0;
padding: 7px 8px;
border-radius: 8px;
background: rgba(248, 90, 84, 0.12);
color: #c3312c;
border: 1px solid rgba(248, 90, 84, 0.32);
font-size: 12px;
}
#${PANEL_ID} .bdc-info {
margin-top: 10px;
padding: 8px;
border-radius: 8px;
background: #f6f7f8;
color: #18191c;
}
#${PANEL_ID} .bdc-line {
display: flex;
justify-content: space-between;
gap: 8px;
margin: 4px 0;
}
#${PANEL_ID} .bdc-key { color: #61666d; white-space: nowrap; }
#${PANEL_ID} .bdc-value {
min-width: 0;
text-align: right;
word-break: break-all;
}
#${PANEL_ID} .bdc-log {
margin-top: 8px;
max-height: 128px;
overflow: auto;
padding: 8px;
border-radius: 8px;
background: #18191c;
color: #e3e5e7;
font-family: Consolas, "Microsoft YaHei Mono", monospace;
font-size: 12px;
white-space: pre-wrap;
}
.bdc-skipped-dynamic {
outline: 2px dashed rgba(255, 159, 10, 0.8) !important;
outline-offset: 2px !important;
opacity: 0.68 !important;
}
`;
document.head.appendChild(style);
}
function createPanel() {
const panel = document.createElement("div");
panel.id = PANEL_ID;
panel.innerHTML = `
Bilibili 半自动动态列表清理
v0.3
条
~
ms
危险:该策略会自动确认所有删除弹窗,包括“删除动态会同时删除视频稿件”。
进度0 / 0
跳过0
状态等待开始
当前动态-
策略普通自动删,视频跳过
`;
document.body.appendChild(panel);
ui.panel = panel;
ui.countInput = panel.querySelector(".bdc-count");
ui.minDelayInput = panel.querySelector(".bdc-min-delay");
ui.maxDelayInput = panel.querySelector(".bdc-max-delay");
ui.modeSelect = panel.querySelector(".bdc-mode");
ui.quick50Button = panel.querySelector(".bdc-quick-50");
ui.quick100Button = panel.querySelector(".bdc-quick-100");
ui.customButton = panel.querySelector(".bdc-custom");
ui.pauseButton = panel.querySelector(".bdc-pause");
ui.stopButton = panel.querySelector(".bdc-stop");
ui.progressText = panel.querySelector(".bdc-progress");
ui.skippedText = panel.querySelector(".bdc-skipped");
ui.statusText = panel.querySelector(".bdc-status");
ui.itemCountText = panel.querySelector(".bdc-item-count");
ui.modeText = panel.querySelector(".bdc-mode-text");
ui.dangerTip = panel.querySelector(".bdc-danger-tip");
ui.logBox = panel.querySelector(".bdc-log");
ui.quick50Button.addEventListener("click", () => startRun(50));
ui.quick100Button.addEventListener("click", () => startRun(100));
ui.customButton.addEventListener("click", () => {
const parsed = parseDeleteCount(ui.countInput.value);
if (parsed == null) return;
startRun(parsed);
});
ui.pauseButton.addEventListener("click", togglePause);
ui.stopButton.addEventListener("click", requestStop);
ui.modeSelect.addEventListener("change", () => {
state.mode = ui.modeSelect.value;
updateUi();
log(`删除策略已切换:${getModeLabel(state.mode)}。`);
});
}
function parseDeleteCount(value) {
const n = Number.parseInt(String(value).trim(), 10);
if (!Number.isFinite(n) || Number.isNaN(n)) {
setStatus("删除数量不是有效数字");
log("删除数量不是有效数字。");
return null;
}
if (n < LIMITS.minDeleteCount) {
setStatus(`删除数量必须 >= ${LIMITS.minDeleteCount}`);
log(`删除数量必须 >= ${LIMITS.minDeleteCount}。`);
return null;
}
if (n > LIMITS.maxDeleteCount) {
setStatus(`删除数量最多 ${LIMITS.maxDeleteCount}`);
log(`删除数量超过上限:${LIMITS.maxDeleteCount}。`);
return null;
}
return n;
}
function parseDelayMs(value, name) {
const n = Number.parseInt(String(value).trim(), 10);
if (!Number.isFinite(n) || Number.isNaN(n)) {
setStatus(`${name}不是有效数字`);
log(`${name}不是有效数字。`);
return null;
}
if (n < LIMITS.minDelayMs) {
setStatus(`${name}必须 >= ${LIMITS.minDelayMs}ms`);
log(`${name}过短:至少 ${LIMITS.minDelayMs}ms。`);
return null;
}
if (n > LIMITS.maxDelayMs) {
setStatus(`${name}必须 <= ${LIMITS.maxDelayMs}ms`);
log(`${name}过长:最多 ${LIMITS.maxDelayMs}ms。`);
return null;
}
return n;
}
function readOptions(maxCount) {
const minDelay = parseDelayMs(ui.minDelayInput.value, "最小间隔");
const maxDelay = parseDelayMs(ui.maxDelayInput.value, "最大间隔");
if (minDelay == null || maxDelay == null) return null;
if (maxDelay < minDelay) {
setStatus("最大间隔不能小于最小间隔");
log("最大间隔不能小于最小间隔。");
return null;
}
if (minDelay < LIMITS.recommendedMinDelayMs) {
log(`提示:最小间隔低于建议值 ${LIMITS.recommendedMinDelayMs}ms,B站前端可能跟不上。`);
}
return {
maxCount,
minDelay,
maxDelay,
mode: ui.modeSelect.value,
};
}
function startRun(maxCount) {
if (state.running) {
log("已有清理任务正在运行,先暂停或停止当前任务。");
return;
}
const options = readOptions(maxCount);
if (!options) return;
if (options.mode === MODE.FORCE_CONFIRM_ALL) {
const ok = window.confirm(
"危险:当前策略会自动确认所有删除弹窗,包括‘删除动态会同时删除视频稿件’。\n\n确认继续执行?"
);
if (!ok) {
setStatus("已取消危险策略启动");
log("用户取消了危险策略启动。");
return;
}
}
ui.countInput.value = String(options.maxCount);
state.running = true;
state.paused = false;
state.stopRequested = false;
state.target = options.maxCount;
state.deleted = 0;
state.skipped = 0;
state.failures = 0;
state.loadMoreFailures = 0;
state.continuousSkips = 0;
state.mode = options.mode;
state.skippedFingerprints = new Set();
clearSkippedMarks();
setStatus(`开始:目标 ${options.maxCount} 条`);
log(`开始清理:目标 ${options.maxCount} 条,间隔 ${options.minDelay}~${options.maxDelay}ms,策略:${getModeLabel(options.mode)}。`);
lockInputs(true);
updateUi();
runLoop(options).catch((error) => {
state.running = false;
state.paused = false;
lockInputs(false);
setStatus("异常停止");
log(`异常:${error && error.message ? error.message : String(error)}`);
updateUi();
});
}
async function runLoop(options) {
while (state.running && !state.stopRequested && state.deleted < options.maxCount) {
await waitWhilePaused();
if (!state.running || state.stopRequested) break;
const stats = getDynamicStats();
updateItemCount(stats);
if (stats.total === 0) {
const loaded = await tryLoadMore("当前没有动态项");
if (!loaded) break;
continue;
}
if (stats.candidates === 0) {
const loaded = await tryLoadMore("当前已加载动态均已跳过,尝试加载更多");
if (!loaded) break;
continue;
}
if (stats.candidates <= LIMITS.lowCandidateThreshold && state.deleted < options.maxCount) {
await tryLoadMore(`候选库存较低:${stats.candidates} 条`);
}
setStatus(`正在处理:已删 ${state.deleted} / ${options.maxCount}`);
updateUi();
const result = await handleNextCandidateOnce(options.mode);
if (result.deleted) {
state.deleted += 1;
state.failures = 0;
state.loadMoreFailures = 0;
state.continuousSkips = 0;
setStatus(`已删除 ${state.deleted} / ${options.maxCount}`);
log(`成功删除:${state.deleted} / ${options.maxCount}`);
updateUi();
if (state.deleted >= options.maxCount) break;
const delay = randomInt(options.minDelay, options.maxDelay);
await sleepInterruptible(delay);
} else if (result.skipped) {
state.skipped += 1;
state.continuousSkips += 1;
state.failures = 0;
setStatus(`已跳过 ${state.skipped} 条,继续找下一个`);
log(`已跳过:${result.reason}`);
updateUi();
if (state.continuousSkips >= LIMITS.maxContinuousSkips) {
setStatus("连续跳过过多,已停止");
log(`连续跳过达到 ${LIMITS.maxContinuousSkips} 次,为避免死循环,停止。`);
break;
}
await sleepInterruptible(250);
} else {
state.failures += 1;
setStatus("删除失败,已停止");
log(`删除失败:${result.reason}`);
break;
}
}
const stoppedByUser = state.stopRequested;
const completed = state.deleted >= options.maxCount;
state.running = false;
state.paused = false;
state.stopRequested = false;
lockInputs(false);
if (completed) {
setStatus(`完成:已删除 ${state.deleted} 条,跳过 ${state.skipped} 条`);
log(`任务完成:已删除 ${state.deleted} 条,跳过 ${state.skipped} 条。`);
} else if (stoppedByUser) {
setStatus(`已停止:已删除 ${state.deleted} 条,跳过 ${state.skipped} 条`);
log(`用户停止:已删除 ${state.deleted} 条,跳过 ${state.skipped} 条。`);
} else if (!state.status.includes("失败") && !state.status.includes("没有更多") && !state.status.includes("停止")) {
setStatus(`结束:已删除 ${state.deleted} 条,跳过 ${state.skipped} 条`);
log(`任务结束:已删除 ${state.deleted} 条,跳过 ${state.skipped} 条。`);
}
updateUi();
}
async function handleNextCandidateOnce(mode) {
const candidate = getNextCandidateItem();
if (!candidate) {
return { deleted: false, skipped: false, reason: "找不到未跳过的候选动态" };
}
const beforeFingerprint = getItemFingerprint(candidate);
const beforeCount = getDynamicItems().length;
candidate.scrollIntoView({ block: "center", behavior: "smooth" });
await sleep(250);
const moreButton = candidate.querySelector(SELECTORS.moreButton);
if (!moreButton) {
markSkipped(candidate, beforeFingerprint, "找不到三个点按钮");
return { deleted: false, skipped: true, reason: "候选动态找不到三个点按钮,已跳过" };
}
smartClick(moreButton);
const deleteMenuItem = await waitFor(() => findDeleteMenuItem(candidate), LIMITS.waitMenuMs, 100);
if (!deleteMenuItem) {
markSkipped(candidate, beforeFingerprint, "没有删除菜单项");
return { deleted: false, skipped: true, reason: "点击三个点后没有找到菜单项“删除”,已跳过" };
}
smartClick(deleteMenuItem);
const modalInfo = await waitFor(findActiveDeleteModal, LIMITS.waitModalMs, 100);
if (!modalInfo) {
return { deleted: false, skipped: false, reason: "点击删除后,没有找到删除确认弹窗" };
}
log(`识别弹窗:${getModalTypeLabel(modalInfo.type)}。`);
if (mode === MODE.SAFE_SKIP_VIDEO) {
if (modalInfo.type === MODAL_TYPE.NORMAL_DYNAMIC) {
return await confirmAndWaitRemoved(modalInfo, candidate, beforeFingerprint, beforeCount, "普通动态");
}
if (modalInfo.type === MODAL_TYPE.VIDEO_ARCHIVE) {
const canceled = await cancelModal(modalInfo);
if (!canceled) {
return { deleted: false, skipped: false, reason: "视频稿件联删弹窗出现,但取消按钮未生效" };
}
markSkipped(candidate, beforeFingerprint, "视频稿件联删默认跳过");
return { deleted: false, skipped: true, reason: "视频稿件联删弹窗已取消,动态已标记跳过" };
}
const canceled = await cancelModal(modalInfo);
if (canceled) {
markSkipped(candidate, beforeFingerprint, "未知删除弹窗默认跳过");
return { deleted: false, skipped: true, reason: "未知删除弹窗已取消,动态已标记跳过" };
}
return { deleted: false, skipped: false, reason: "未知删除弹窗,且无法取消" };
}
// 强制策略:只要找到确认删除按钮,就确认。
return await confirmAndWaitRemoved(modalInfo, candidate, beforeFingerprint, beforeCount, getModalTypeLabel(modalInfo.type));
}
async function confirmAndWaitRemoved(modalInfo, candidate, beforeFingerprint, beforeCount, contextLabel) {
const confirmButton = modalInfo.confirmButton || findConfirmDeleteButton(modalInfo.wrap);
if (!confirmButton) {
return { deleted: false, skipped: false, reason: `${contextLabel}:没有找到“确认删除”按钮` };
}
smartClick(confirmButton);
const removed = await waitFor(() => {
if (!document.body.contains(candidate)) return true;
const afterItems = getDynamicItems();
if (afterItems.length < beforeCount) return true;
if (!afterItems.some((item) => getItemFingerprint(item) === beforeFingerprint)) return true;
return false;
}, LIMITS.waitRemovedMs, 120);
if (!removed) {
const stillModal = findActiveDeleteModal();
if (stillModal) {
return { deleted: false, skipped: false, reason: `${contextLabel}:确认删除后弹窗仍存在,等待 DOM 变化超时;本轮不计数` };
}
return { deleted: false, skipped: false, reason: `${contextLabel}:弹窗已消失但动态未确认移除,等待 DOM 变化超时;本轮不计数` };
}
updateItemCount(getDynamicStats());
return { deleted: true, skipped: false, reason: `${contextLabel} 已删除` };
}
async function cancelModal(modalInfo) {
const cancelButton = modalInfo.cancelButton || findCancelButton(modalInfo.wrap);
if (!cancelButton) return false;
smartClick(cancelButton);
const gone = await waitFor(() => !document.body.contains(modalInfo.wrap) || !isVisible(modalInfo.wrap), LIMITS.waitModalGoneMs, 100);
return Boolean(gone);
}
function getDynamicItems() {
return Array.from(document.querySelectorAll(SELECTORS.item))
.filter((el) => el instanceof HTMLElement && isVisible(el));
}
function getNextCandidateItem() {
const items = getDynamicItems();
for (const item of items) {
if (item.dataset.bdcSkipped === "1") continue;
const fingerprint = getItemFingerprint(item);
if (state.skippedFingerprints.has(fingerprint)) {
item.dataset.bdcSkipped = "1";
item.classList.add("bdc-skipped-dynamic");
continue;
}
return item;
}
return null;
}
function getDynamicStats() {
const items = getDynamicItems();
const candidates = items.filter((item) => {
const fingerprint = getItemFingerprint(item);
return item.dataset.bdcSkipped !== "1" && !state.skippedFingerprints.has(fingerprint);
});
return {
total: items.length,
candidates: candidates.length,
skippedLoaded: items.length - candidates.length,
};
}
function getItemFingerprint(item) {
if (!item) return "";
const link = item.querySelector("a[href*='/opus/'], a[href*='/video/'], a[href*='t.bilibili.com']");
const href = link ? link.getAttribute("href") : "";
const text = normalizeText(item.innerText).slice(0, 240);
return `${href}::${text}`;
}
function markSkipped(item, fingerprint, reason) {
if (!item) return;
const fp = fingerprint || getItemFingerprint(item);
state.skippedFingerprints.add(fp);
item.dataset.bdcSkipped = "1";
item.dataset.bdcSkipReason = reason || "已跳过";
item.classList.add("bdc-skipped-dynamic");
}
function clearSkippedMarks() {
for (const item of document.querySelectorAll(SELECTORS.item)) {
if (!(item instanceof HTMLElement)) continue;
delete item.dataset.bdcSkipped;
delete item.dataset.bdcSkipReason;
item.classList.remove("bdc-skipped-dynamic");
}
}
function findDeleteMenuItem(candidate) {
const labelsInCandidate = Array.from(candidate.querySelectorAll(SELECTORS.menuLabel));
const scoped = findDeleteLabelFromLabels(labelsInCandidate);
if (scoped) return scoped;
// 兜底:有些浮层可能挂到 body 下。只认 cascader 的 label,且文本必须严格等于“删除”。
const labels = Array.from(document.querySelectorAll(SELECTORS.menuLabel));
return findDeleteLabelFromLabels(labels);
}
function findDeleteLabelFromLabels(labels) {
for (const label of labels) {
if (!(label instanceof HTMLElement)) continue;
if (!isVisible(label)) continue;
if (normalizeText(label.textContent) !== "删除") continue;
const menuItem = label.closest(SELECTORS.menuItem);
return menuItem || label;
}
return null;
}
function findActiveDeleteModal() {
const wraps = Array.from(document.querySelectorAll(SELECTORS.modalWrap))
.filter((wrap) => wrap instanceof HTMLElement && isVisible(wrap));
for (let i = wraps.length - 1; i >= 0; i -= 1) {
const wrap = wraps[i];
const text = normalizeText(wrap.innerText);
if (!text.includes("确认删除") && !text.includes("取消")) continue;
if (!text.includes("删除")) continue;
const title = normalizeText((wrap.querySelector(SELECTORS.modalTitle) || {}).textContent || "");
const content = normalizeText((wrap.querySelector(SELECTORS.modalContent) || {}).textContent || "");
const confirmButton = findConfirmDeleteButton(wrap);
const cancelButton = findCancelButton(wrap);
const type = classifyDeleteModal(title, content, text);
return { wrap, title, content, text, confirmButton, cancelButton, type };
}
return null;
}
function classifyDeleteModal(title, content, fullText) {
const text = `${title}::${content}::${fullText}`;
if (text.includes("删除动态会同时删除视频稿件") || text.includes("视频删除后将无法恢复")) {
return MODAL_TYPE.VIDEO_ARCHIVE;
}
if (text.includes("要删除动态吗") || text.includes("动态删除后将无法恢复")) {
return MODAL_TYPE.NORMAL_DYNAMIC;
}
return MODAL_TYPE.UNKNOWN_DELETE;
}
function findConfirmDeleteButton(scope = document) {
const redConfirmButtons = Array.from(scope.querySelectorAll(SELECTORS.confirmButton));
for (const button of redConfirmButtons) {
if (!(button instanceof HTMLElement)) continue;
if (!isVisible(button)) continue;
if (normalizeText(button.textContent) === "确认删除") return button;
}
const modalButtons = Array.from(scope.querySelectorAll(SELECTORS.modalButton));
for (const button of modalButtons) {
if (!(button instanceof HTMLElement)) continue;
if (!isVisible(button)) continue;
if (normalizeText(button.textContent) === "确认删除") return button;
}
return null;
}
function findCancelButton(scope = document) {
const cancelButtons = Array.from(scope.querySelectorAll(SELECTORS.cancelButton));
for (const button of cancelButtons) {
if (!(button instanceof HTMLElement)) continue;
if (!isVisible(button)) continue;
if (normalizeText(button.textContent) === "取消") return button;
}
const modalButtons = Array.from(scope.querySelectorAll(SELECTORS.modalButton));
for (const button of modalButtons) {
if (!(button instanceof HTMLElement)) continue;
if (!isVisible(button)) continue;
if (normalizeText(button.textContent) === "取消") return button;
}
return null;
}
async function tryLoadMore(reason) {
if (state.loadMoreFailures >= LIMITS.maxLoadMoreFailures) {
setStatus("没有更多动态或加载失败");
log(`补加载停止:连续 ${state.loadMoreFailures} 次没有加载更多。`);
return false;
}
const beforeStats = getDynamicStats();
setStatus(`${reason},尝试加载更多`);
log(`${reason},滚动到底部尝试加载更多。`);
updateUi();
window.scrollTo({ top: document.documentElement.scrollHeight || document.body.scrollHeight, behavior: "smooth" });
const loaded = await waitFor(() => {
const afterStats = getDynamicStats();
return afterStats.total > beforeStats.total || afterStats.candidates > beforeStats.candidates;
}, LIMITS.waitLoadMoreMs, 150);
const afterStats = getDynamicStats();
updateItemCount(afterStats);
if (loaded || afterStats.total > beforeStats.total || afterStats.candidates > beforeStats.candidates) {
state.loadMoreFailures = 0;
log(`加载更多成功:总数 ${beforeStats.total} → ${afterStats.total},候选 ${beforeStats.candidates} → ${afterStats.candidates}。`);
return true;
}
state.loadMoreFailures += 1;
log(`加载更多未发现新增:总数 ${beforeStats.total} → ${afterStats.total},失败 ${state.loadMoreFailures}/${LIMITS.maxLoadMoreFailures}。`);
return state.loadMoreFailures < LIMITS.maxLoadMoreFailures;
}
function togglePause() {
if (!state.running) return;
state.paused = !state.paused;
setStatus(state.paused ? "已暂停" : "继续运行");
log(state.paused ? "已暂停。" : "继续运行。");
updateUi();
}
function requestStop() {
if (!state.running) return;
state.stopRequested = true;
state.paused = false;
setStatus("正在停止,本轮结束后退出");
log("收到停止请求:本轮安全结束后退出。");
updateUi();
}
async function waitWhilePaused() {
while (state.running && state.paused && !state.stopRequested) {
await sleep(200);
}
}
async function sleepInterruptible(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
if (!state.running || state.stopRequested) return;
await waitWhilePaused();
await sleep(Math.min(200, ms - (Date.now() - start)));
}
}
function lockInputs(locked) {
ui.countInput.disabled = locked;
ui.minDelayInput.disabled = locked;
ui.maxDelayInput.disabled = locked;
ui.modeSelect.disabled = locked;
ui.quick50Button.disabled = locked;
ui.quick100Button.disabled = locked;
ui.customButton.disabled = locked;
ui.pauseButton.disabled = !locked;
ui.stopButton.disabled = !locked;
}
function updateUi() {
if (!ui.panel) return;
ui.progressText.textContent = `${state.deleted} / ${state.target || 0}`;
ui.skippedText.textContent = String(state.skipped);
ui.statusText.textContent = state.status;
ui.pauseButton.textContent = state.paused ? "继续" : "暂停";
ui.modeText.textContent = getModeLabel(state.mode);
if (ui.dangerTip) {
ui.dangerTip.style.display = state.mode === MODE.FORCE_CONFIRM_ALL ? "block" : "none";
}
updateItemCount(getDynamicStats());
renderLogs();
}
function updateItemCount(stats) {
if (!ui.itemCountText) return;
const data = typeof stats === "number" ? { total: stats, candidates: stats, skippedLoaded: 0 } : stats;
ui.itemCountText.textContent = `${data.candidates}/${data.total} 条候选`;
}
function setStatus(text) {
state.status = text;
if (ui.statusText) ui.statusText.textContent = text;
}
function log(message) {
const time = new Date().toLocaleTimeString("zh-CN", { hour12: false });
state.logs.push(`[${time}] ${message}`);
if (state.logs.length > 10) state.logs.shift();
renderLogs();
}
function renderLogs() {
if (!ui.logBox) return;
ui.logBox.textContent = state.logs.join("\n");
ui.logBox.scrollTop = ui.logBox.scrollHeight;
}
function getModeLabel(mode) {
if (mode === MODE.FORCE_CONFIRM_ALL) return "危险:全部确认删除";
return "普通自动删,视频跳过";
}
function getModalTypeLabel(type) {
if (type === MODAL_TYPE.NORMAL_DYNAMIC) return "普通动态删除";
if (type === MODAL_TYPE.VIDEO_ARCHIVE) return "视频稿件联删";
return "未知删除弹窗";
}
function normalizeText(text) {
return String(text || "").replace(/\s+/g, "").trim();
}
function isVisible(element) {
if (!(element instanceof HTMLElement)) return false;
const style = window.getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function smartClick(element) {
if (!element) return;
const target = element instanceof HTMLElement ? element : element.parentElement;
if (!target) return;
target.scrollIntoView({ block: "center", behavior: "smooth" });
const eventOptions = { bubbles: true, cancelable: true, view: window };
for (const type of ["pointerover", "mouseover", "mouseenter", "pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
try {
target.dispatchEvent(new MouseEvent(type, eventOptions));
} catch (_) {
// 部分浏览器不支持某些事件类型时,直接忽略。
}
}
if (typeof target.click === "function") target.click();
}
function waitFor(predicate, timeoutMs, intervalMs) {
const startedAt = Date.now();
return new Promise((resolve) => {
const timer = window.setInterval(() => {
let value = null;
try {
value = predicate();
} catch (_) {
value = null;
}
if (value) {
window.clearInterval(timer);
resolve(value);
return;
}
if (Date.now() - startedAt >= timeoutMs) {
window.clearInterval(timer);
resolve(null);
}
}, intervalMs);
});
}
function sleep(ms) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init, { once: true });
} else {
init();
}
})();