B站评论自动展开 v1

自动持续点击“查看全部/查看更多回复”,一键展开楼中楼;支持 Alt+E 开关(持久化)、SPA 导航自适应、右下角状态徽章。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         B站评论自动展开 v1
// @namespace    qlt-auto-expand-bili-comments
// @version      0.2.1
// @description  自动持续点击“查看全部/查看更多回复”,一键展开楼中楼;支持 Alt+E 开关(持久化)、SPA 导航自适应、右下角状态徽章。
// @author       qiletian
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/bangumi/play/*
// @match        https://www.bilibili.com/read/*
// @match        https://www.bilibili.com/opus/*
// @icon         https://www.bilibili.com/favicon.ico
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const Config = {
    scanInterval: 700,            // 扫描周期(ms)
    jitter: [100, 250],           // 每次点击的随机抖动(ms)
    maxClicksPerTick: 8,          // 单轮最多点击几个“查看更多”
    maxTotalClicks: 5000,         // 安全阈值,避免过度请求
    autoStart: true,              // 首次无持久化记录时的默认值
  };

  // —— 状态持久化 —— //
  const STORE_KEY = 'biliAutoExpand.enabled.v1';
  const saveEnabled = (v) => {
    try { localStorage.setItem(STORE_KEY, v ? '1' : '0'); } catch (_) {}
  };
  const loadEnabled = (fallback = Config.autoStart) => {
    try {
      const v = localStorage.getItem(STORE_KEY);
      if (v === null) return fallback;
      return v === '1';
    } catch (_) { return fallback; }
  };

  let enabled      = loadEnabled();       // 关键:初始化时从 localStorage 读取
  let totalClicks  = 0;
  let clickedButtons = new WeakSet();     // 记忆已点过按钮,避免重复
  let timer        = null;                // setInterval 句柄
  let currentApp   = null;                // 缓存当前评论 App 节点
  let appObserver  = null;                // 监听评论区内部变化的观察器
  let bootTimer    = null;                // 防抖重引导

  const log  = (...args) => console.debug('[Bili-AutoExpand]', ...args);
  const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

  function waitForCommentApp(timeout = 30000) {
    return new Promise((resolve, reject) => {
      const t0 = Date.now();
      (function check() {
        const app = document.querySelector('#commentapp > bili-comments');
        if (app?.shadowRoot) return resolve(app);
        if (Date.now() - t0 > timeout) return reject(new Error('commentapp not found'));
        requestAnimationFrame(check);
      })();
    });
  }

  function findViewMoreButtons(app) {
    const out = [];
    try {
      const threads = app.shadowRoot.querySelectorAll('#feed > bili-comment-thread-renderer');
      threads.forEach(thread => {
        const tsr = thread.shadowRoot;
        if (!tsr) return;
        const replies = tsr.querySelector('#replies > bili-comment-replies-renderer');
        if (!replies?.shadowRoot) return;

        // 标准容器:#view-more > bili-text-button > button
        const textBtn = replies.shadowRoot.querySelector('#view-more > bili-text-button');
        const btn = textBtn?.shadowRoot?.querySelector('button');
        if (btn && !btn.disabled && btn.offsetParent !== null && !clickedButtons.has(btn)) {
          out.push(btn);
        }

        // 兜底:标题文案有时会变化
        replies.shadowRoot.querySelectorAll('button').forEach(b => {
          const t = (b.textContent || '').trim();
          if (!clickedButtons.has(b) && /查看更多|查看全部|展开更多|更多回复|View More/i.test(t)) {
            out.push(b);
          }
        });
      });
    } catch (_) { /* ignore */ }
    return Array.from(new Set(out));
  }

  function clickWithJitter(btn) {
    return new Promise(res => {
      setTimeout(() => {
        try {
          btn.click();
          clickedButtons.add(btn);
          totalClicks++;
        } catch (_) { /* ignore */ }
        res();
      }, rand(Config.jitter[0], Config.jitter[1]));
    });
  }

  async function tick(app) {
    if (!enabled) return;
    const list = findViewMoreButtons(app);
    if (list.length === 0) return;
    const n = Math.min(list.length, Config.maxClicksPerTick);
    for (let i = 0; i < n; i++) {
      if (totalClicks >= Config.maxTotalClicks) { enabled = false; saveEnabled(enabled); paintBadge(); stopLoop(); break; }
      await clickWithJitter(list[i]);
    }
  }

  function startLoop(app) {
    stopLoop();
    if (!enabled) return; // 关键:尊重开关状态
    timer = setInterval(() => tick(app), Config.scanInterval);
  }

  function stopLoop() {
    if (timer) clearInterval(timer);
    timer = null;
  }

  function paintBadge() {
    let el = document.getElementById('bili-autoexpand-badge');
    if (!el) {
      el = document.createElement('div');
      el.id = 'bili-autoexpand-badge';
      Object.assign(el.style, {
        position: 'fixed', right: '10px', bottom: '10px', zIndex: 2147483647,
        background: '#00a1d6', color: '#fff', padding: '6px 10px',
        borderRadius: '8px', fontSize: '12px', boxShadow: '0 2px 6px rgba(0,0,0,.2)',
        userSelect: 'none', cursor: 'pointer'
      });
      el.addEventListener('click', toggle);
      document.body.appendChild(el);
    }
    el.textContent = `评论自动展开:${enabled ? '开' : '关'}(Alt+E)`;
    el.style.background = enabled ? '#00a1d6' : '#666';
  }

  function toggle() {
    enabled = !enabled;
    saveEnabled(enabled);     // 关键:持久化到 localStorage
    paintBadge();
    if (currentApp) {
      enabled ? startLoop(currentApp) : stopLoop();
    }
  }

  function onKeydown(e) {
    // 兼容性:忽略组合输入与重复触发
    if (e.repeat) return;
    const key = (e.key || '').toLowerCase();
    if (e.altKey && key === 'e') toggle();
  }

  function observeUrlChange() {
    // 监听 URL 变化:B 站常用 history.pushState
    let last = location.href;
    const scheduleBoot = () => {
      clearTimeout(bootTimer);
      bootTimer = setTimeout(boot, 150); // 防抖,避免频繁重启
    };
    const obs = new MutationObserver(() => {
      if (location.href !== last) {
        last = location.href;
        scheduleBoot();
      }
    });
    obs.observe(document, { subtree: true, childList: true });
  }

  function resetAppObserver() {
    if (appObserver) {
      try { appObserver.disconnect(); } catch (_) {}
      appObserver = null;
    }
  }

  async function boot() {
    stopLoop();
    totalClicks = 0;
    clickedButtons = new WeakSet();
    resetAppObserver();

    try {
      currentApp = await waitForCommentApp();
      paintBadge();

      // 仅在开启状态下启动扫描循环
      if (enabled) startLoop(currentApp);
      // 监听评论区内部异步加载
      appObserver = new MutationObserver(() => {
        if (enabled) startLoop(currentApp);
        else stopLoop();
      });
      appObserver.observe(currentApp.shadowRoot, { childList: true, subtree: true });

      document.removeEventListener('keydown', onKeydown);
      document.addEventListener('keydown', onKeydown);

      log('ready, enabled=', enabled);
    } catch (e) {
      log('comment app not found in time', e);
    }
  }

  // 启动
  boot();
  observeUrlChange();
})();