// ==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(); } })();