// ==UserScript== // @name B站内容屏蔽(bili_rebuild改) // @namespace dreamcenter // @version 4.0.0 // @description B站评论过滤器 · 可视化配置界面 // @license MIT // @match *://*.bilibili.com/video/* // @match *://*.bilibili.com/opus/* // @match *://space.bilibili.com/* // @match *://t.bilibili.com/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function () { 'use strict'; /* 悬浮窗按钮开关 * true = 显示右下角 FILTER 悬浮按钮 * false = 隐藏悬浮按钮(配置面板将无法从页面打开) * 修改后重新加载脚本即可生效 */ const SHOW_FAB = true; /* ================================================================ * 默认配置(首次使用 / 点击「重置」时恢复) * 日常修改请使用页面右下角的 FILTER 面板 * ================================================================ */ const DEFAULT_CONFIG = { // 文本关键词屏蔽 banMap: ['例子'], // 屏蔽指定用户 banUser: ['例子'], // 等级屏蔽(level_0~level_6,level_h = 加闪电六级号) banLevel: ['level_0'], // 表情屏蔽(表情的 alt 值,F12 检查后复制) banEmoji: [], // 屏蔽 @某人 banAt: ['例子'], // 屏蔽全部笔记 banExpand: false, // 调试:控制台日志 logMode: false, // 调试:红色标记而不隐藏 redLine: false, // 悬浮按钮(null = 未自定义,使用默认右下角) fabX: null, // 距视口左侧 px fabTop: null, // 跟随屏幕(开启):距视口顶部 px fabDocY: null, // 钉在网页(关闭):文档纵向位置 px fabFollow: false, // true = 跟随屏幕滚动保持可见 / false = 钉在网页内容上随页面移动 // 主题:auto = 跟随系统 / light = 浅色 / dark = 深色 theme: 'auto', // 全局过滤开关(面板可随时切换) enabled: true, // 正则表达式屏蔽(整条评论文本匹配,支持正则语法) banRegex: [], // 按 UID 屏蔽用户(改昵称也不受影响) banUid: [] }; const STORAGE_KEY = 'bili_rebuild_config_v4'; const LEVELS = ['level_0', 'level_1', 'level_2', 'level_3', 'level_4', 'level_5', 'level_6', 'level_h']; /* ================================================================ * 持久化(GM 存储优先,失败则回退 localStorage) * ================================================================ */ function deepClone(o) { return JSON.parse(JSON.stringify(o)); } // 配置字段白名单与类型校验(防止损坏/恶意备份或旧版存储导致脚本异常) const CONFIG_ARRAY_KEYS = ['banMap', 'banUser', 'banLevel', 'banEmoji', 'banAt', 'banRegex', 'banUid']; const CONFIG_BOOL_KEYS = ['banExpand', 'logMode', 'redLine', 'fabFollow', 'enabled']; const CONFIG_NUM_KEYS = ['fabX', 'fabTop', 'fabDocY']; function sanitizeConfig(obj) { const merged = deepClone(DEFAULT_CONFIG); for (const k of CONFIG_ARRAY_KEYS) if (k in obj && Array.isArray(obj[k])) merged[k] = obj[k].filter(v => typeof v === 'string'); for (const k of CONFIG_BOOL_KEYS) if (k in obj) merged[k] = !!obj[k]; for (const k of CONFIG_NUM_KEYS) if (k in obj && (typeof obj[k] === 'number' || obj[k] === null)) merged[k] = obj[k]; if (obj.theme === 'light' || obj.theme === 'dark' || obj.theme === 'auto') merged.theme = obj.theme; return merged; } function loadConfig() { let raw = null; try { if (typeof GM_getValue === 'function') raw = GM_getValue(STORAGE_KEY, null); } catch (e) { } if (!raw) { try { raw = localStorage.getItem(STORAGE_KEY); } catch (e) { } } if (raw) { try { const obj = JSON.parse(raw); return sanitizeConfig(obj); } catch (e) { } } return deepClone(DEFAULT_CONFIG); } function saveConfig() { const raw = JSON.stringify(config); try { if (typeof GM_setValue === 'function') GM_setValue(STORAGE_KEY, raw); } catch (e) { } try { localStorage.setItem(STORAGE_KEY, raw); } catch (e) { } } let config = loadConfig(); /* ================================================================ * 评论过滤引擎(逻辑与原版一致,数据源改为动态 config) * ================================================================ */ const filterFunction = { bySpan(raw) { for (const key of config.banMap) if (raw.includes(key)) return true; }, byImg(raw) { for (const key of config.banEmoji) if (raw.includes(key)) return true; }, byAt(raw) { for (const key of config.banAt) if (raw.includes(key)) return true; }, bySearch(raw) { for (const key of config.banMap) if (raw.includes(key)) return true; } }; /* ========== 正则屏蔽:异步 + 硬超时(防 ReDoS) ========== */ const REGEX_TIMEOUT_MS = 16; // 单轮正则匹配硬超时(毫秒) // 用于正则添加时的耗时基准文本(大量重复字符,放大回溯开销) const BENCH_TEXT = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + 'B站评论过滤基准' + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; // 常见 ReDoS 危险模式预检(嵌套/重叠量词、双重闭合、交替分组量词等) const DANGEROUS_REGEX = /\([^)]*\)[+*][^)]*\)[+*]|\([^)]*\|[^)]*\)[+*]|\+\+|\*\*|\.\*\.\*|\.\*[+*]/; // 正则匹配在 Web Worker 中执行,主线程用定时器硬超时;超时即终止 Worker 重建,并将该轮正则拉黑 const REGEX_WORKER_SRC = ` self.onmessage = function (e) { var d = e.data, matched = false; for (var i = 0; i < d.patterns.length; i++) { try { if (new RegExp(d.patterns[i]).test(d.text)) { matched = true; break; } } catch (err) {} } self.postMessage({ id: d.id, matched: matched }); };`; let regexWorker = null; let regexWorkerBusy = false; let regexWorkerQueue = []; let regexTaskId = 0; let currentRegexTask = null; let currentRegexTaskId = -1; let regexWorkerTimer = null; let regexWorkerDisabled = false; // Worker 不可用(CSP 限制/创建失败/连续超时)时置真,走同步兜底 let regexTimeoutCount = 0; let regexBlacklist = new Set(); // 曾超时的危险正则(会话级跳过,避免反复拖慢) function ensureRegexWorker() { if (regexWorker) return regexWorker; if (regexWorkerDisabled) return null; try { const blob = new Blob([REGEX_WORKER_SRC], { type: 'application/javascript' }); regexWorker = new Worker(URL.createObjectURL(blob)); regexWorker.onmessage = (ev) => { const d = ev.data || {}; if (d.id !== currentRegexTaskId) return; const task = currentRegexTask; currentRegexTask = null; currentRegexTaskId = -1; regexWorkerBusy = false; clearTimeout(regexWorkerTimer); if (task) task.resolve(!!d.matched); pumpRegexQueue(); }; } catch (e) { regexWorker = null; regexWorkerDisabled = true; } return regexWorker; } // Worker 不可用时的同步兜底:整体时间预算 + 单条超时拉黑 + 跳过危险模式,尽量防 ReDoS function testRegexSyncFallback(text, patterns) { const budgetStart = performance.now(); for (const p of patterns) { if (performance.now() - budgetStart > REGEX_TIMEOUT_MS) break; if (DANGEROUS_REGEX.test(p)) { regexBlacklist.add(p); continue; } // 危险模式直接跳过,避免同步卡死 const t0 = performance.now(); let matched = false; try { matched = new RegExp(p).test(text || ''); } catch (e) { } if (performance.now() - t0 > REGEX_TIMEOUT_MS) regexBlacklist.add(p); if (matched) return true; } return false; } function pumpRegexQueue() { if (regexWorkerBusy || !regexWorkerQueue.length) return; const task = regexWorkerQueue.shift(); const patterns = (task.patterns || []).filter(p => !regexBlacklist.has(p)); if (!patterns.length) { task.resolve(false); setTimeout(pumpRegexQueue, 0); return; } regexWorkerBusy = true; currentRegexTask = task; currentRegexTaskId = ++regexTaskId; const w = ensureRegexWorker(); if (!w) { regexWorkerBusy = false; // Worker 不可用(CSP/创建失败):同步兜底匹配(异步调度避免深递归爆栈) task.resolve(testRegexSyncFallback(task.text, patterns)); setTimeout(pumpRegexQueue, 0); return; } w.postMessage({ id: currentRegexTaskId, text: task.text, patterns: patterns }); regexWorkerTimer = setTimeout(() => { const t = currentRegexTask; currentRegexTask = null; currentRegexTaskId = -1; regexWorkerBusy = false; try { regexWorker.terminate(); } catch (e) { } regexWorker = null; // 不在超时处整轮拉黑(避免误伤正常正则);由同步兜底按单条耗时精准拉黑 regexTimeoutCount++; if (regexTimeoutCount >= 3) regexWorkerDisabled = true; if (t) t.resolve(testRegexSyncFallback(t.text, patterns)); setTimeout(pumpRegexQueue, 0); }, REGEX_TIMEOUT_MS); } // 异步测试:文本是否命中任一正则(硬超时,超时视为未命中) function testRegexAsync(text, patterns) { return new Promise((resolve) => { regexWorkerQueue.push({ text: text || '', patterns: patterns || [], resolve }); pumpRegexQueue(); }); } // 同步测试(仅用于添加正则时的耗时基准,不参与评论过滤) function testRegexSync(text, pattern) { const t0 = performance.now(); try { new RegExp(pattern).test(text); } catch (e) { } return performance.now() - t0; } // 待异步正则检测的评论队列(关键词未命中时才入队) const pendingRegexChecks = []; let regexDraining = false; function drainRegexQueue() { if (regexDraining) return; regexDraining = true; const loop = () => { const item = pendingRegexChecks.shift(); if (!item) { regexDraining = false; return; } testRegexAsync(item.text, item.patterns).then(hit => { if (hit) item.apply(); loop(); }); }; loop(); } // 判断通用评论屏蔽(同步:关键词/表情/@,正则交由异步路径) function judgeIfBanned(raw) { const fullText = (raw.innerText || raw.textContent || '').trim(); const replyMatch = fullText.match(/^回复\s*(@[^\s]+)\s*[::]/); let targetUser = replyMatch ? replyMatch[1] : null; for (let node of raw.childNodes) { if (node.nodeType === Node.TEXT_NODE) { // 暂无此情形 } if (node.tagName === 'SPAN') { const text = node.innerText; if (text && filterFunction.bySpan(text)) return true; } else if (node.tagName === 'IMG') { if (filterFunction.byImg(node.alt)) return true; } else if (node.tagName === 'A') { var type = node.dataset.type; if (type == "mention") { const atText = node.innerText.trim(); if (targetUser && targetUser === atText) { targetUser = null; continue; } if (filterFunction.byAt(atText)) return true; } if (type == "search") { const searchText = node.innerText.trim(); if (searchText && filterFunction.bySearch(searchText)) return true; } } } return false; } function judgeIfLevelBanned(levelImg) { for (const key of config.banLevel) if (levelImg.includes(key)) return true; return false; } function judgeIfExpandBanned(expandEL) { return config.banExpand && !!expandEL; } function judgeIfUserBanned(userName) { for (const key of config.banUser) if (userName.includes(key)) return true; return false; } // 从评论节点中提取 UID:B站新版评论在 #user-avatar 的 data-user-profile-id 上 function getUid(scopeEl) { if (!scopeEl || !scopeEl.shadowRoot) return null; const avatar = scopeEl.shadowRoot.querySelector('#user-avatar'); const v = avatar && (avatar.dataset.userProfileId || avatar.getAttribute('data-user-profile-id')); if (v) return String(v).trim(); // 回退:扫描空间链接 const links = Array.from(scopeEl.shadowRoot.querySelectorAll('a[href]')); for (const a of links) { const m = (a.href || '').match(/(?:space\.bilibili\.com|space\/)(\d+)/); if (m) return m[1]; } return null; } function judgeIfUidBanned(uid) { if (!uid) return false; for (const key of config.banUid) if (uid === key) return true; return false; } // 统一隐藏/标红逻辑 function hideComment(el, textEl) { if (config.logMode) console.log(textEl.innerText); if (config.redLine) textEl.style.color = "red"; else el.style.display = "none"; } function runCore() { // 全局过滤开关:关闭时不做任何屏蔽 if (!config.enabled) return; var reviews = Array.from(document.querySelectorAll("bili-comments")).flatMap(el => el.shadowRoot ? Array.from(el.shadowRoot.querySelectorAll("#feed > bili-comment-thread-renderer")) : []); for (var review of reviews) { if (review.getAttribute('filtered') == null) { try { var comment = review.shadowRoot.querySelector('#comment'); var level = comment.shadowRoot.querySelector("#header > bili-comment-user-info").shadowRoot.querySelector("#user-level > img").src; var uid = getUid(comment); var userInfo = comment.shadowRoot.querySelector("#header > bili-comment-user-info"); var userName = userInfo.shadowRoot.querySelector("#user-name").innerText.trim(); var commentText = comment.shadowRoot.querySelector("#content > bili-rich-text").shadowRoot.querySelector("#contents"); var expandEL = comment.shadowRoot.querySelector("#expand"); review.setAttribute('filtered', true); // 顺序规则:关键词/等级/用户等同步判断优先,命中直接隐藏,不再走正则 if (judgeIfLevelBanned(level) || judgeIfBanned(commentText) || judgeIfExpandBanned(expandEL) || judgeIfUserBanned(userName) || judgeIfUidBanned(uid)) { hideComment(review, commentText); } else if (config.banRegex.length) { const text = (commentText.innerText || commentText.textContent || '').trim(); if (text) pendingRegexChecks.push({ text, patterns: config.banRegex, apply: () => hideComment(review, commentText) }); } } catch (e) { /* B站结构变化时跳过该条,不影响其它评论 */ } } try { var replies = review.shadowRoot.querySelector("#replies > bili-comment-replies-renderer").shadowRoot.querySelectorAll("#expander-contents > bili-comment-reply-renderer:not([filtered])"); for (var reply of replies) { var replyLevel = reply.shadowRoot.querySelector("#main > bili-comment-user-info").shadowRoot.querySelector("#user-level > img").src; var replyUid = getUid(reply); var replyUserInfo = reply.shadowRoot.querySelector("#main > bili-comment-user-info"); var replyUserName = replyUserInfo.shadowRoot.querySelector("#user-name").innerText.trim(); var replyCommentText = reply.shadowRoot.querySelector("#main > bili-rich-text").shadowRoot.querySelector("#contents"); reply.setAttribute('filtered', true); if (judgeIfLevelBanned(replyLevel) || judgeIfBanned(replyCommentText) || judgeIfUserBanned(replyUserName) || judgeIfUidBanned(replyUid)) { hideComment(reply, replyCommentText); } else if (config.banRegex.length) { const text = (replyCommentText.innerText || replyCommentText.textContent || '').trim(); if (text) pendingRegexChecks.push({ text, patterns: config.banRegex, apply: () => hideComment(reply, replyCommentText) }); } } } catch (e) { /* B站结构变化时跳过该楼回复,不影响其它 */ } } // 异步处理正则队列(关键词未命中的评论) drainRegexQueue(); } // 清除过滤标记与样式,便于配置变更后重新过滤 function resetFilterMarks() { const threads = Array.from(document.querySelectorAll("bili-comments")).flatMap(el => el.shadowRoot ? Array.from(el.shadowRoot.querySelectorAll("bili-comment-thread-renderer")) : []); for (const review of threads) { if (review.hasAttribute('filtered')) { review.removeAttribute('filtered'); review.style.display = ''; const comment = review.shadowRoot.querySelector('#comment'); if (comment) { const rich = comment.shadowRoot && comment.shadowRoot.querySelector('#content > bili-rich-text'); if (rich && rich.shadowRoot) { const cc = rich.shadowRoot.querySelector('#contents'); if (cc) cc.style.color = ''; } } } const replyRoot = review.shadowRoot && review.shadowRoot.querySelector('#replies > bili-comment-replies-renderer'); if (replyRoot) { replyRoot.querySelectorAll('bili-comment-reply-renderer[filtered]').forEach(r => { r.removeAttribute('filtered'); r.style.display = ''; const main = r.shadowRoot.querySelector('#main'); if (main) { const rich = main.shadowRoot && main.shadowRoot.querySelector('bili-rich-text'); if (rich && rich.shadowRoot) { const cc = rich.shadowRoot.querySelector('#contents'); if (cc) cc.style.color = ''; } } }); } } } /* ================================================================ * 过滤引擎启动 * ================================================================ */ // 过滤引擎启动(不覆盖页面 window.onload,避免 load 已触发或被他脚本覆盖) function startEngine() { var startMark = false; setInterval(function () { if (startMark) { runCore(); } else { startMark = document.querySelector("bili-comments") != null; } }, 100); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startEngine); } else { startEngine(); } /* ================================================================ * Arknights 军规风可视化配置面板 * ================================================================ */ const UI_CSS = ` :host { all: initial; } * { box-sizing: border-box; margin: 0; padding: 0; } [hidden] { display: none !important; } /* ===== 默认 = 深色主题 ===== */ .ark { --bg-panel: linear-gradient(180deg, rgba(48,54,62,0.97) 0%, rgba(36,42,50,0.97) 100%); --fab-bg: linear-gradient(180deg, rgba(48,54,62,0.98), rgba(36,42,50,0.98)); --text: #f1f3f5; --muted: #8b919b; --muted-2: #5b6066; --border: rgba(255,255,255,0.22); --border-soft: rgba(255,255,255,0.12); --border-faint: rgba(255,255,255,0.08); --layer: rgba(255,255,255,0.14); --layer-hover: rgba(255,255,255,0.06); --chip-bg: rgba(255,255,255,0.06); --chip-hover: rgba(255,255,255,0.14); --accent-bg: #ffffff; --accent-text: #111213; --accent-hover: #d5d8dc; --danger: #ff4757; --info: #8ec8ff; --corner: rgba(255,255,255,0.85); --watermark: rgba(255,255,255,0.035); --shadow: 0 0 24px rgba(0,0,0,0.55); --thumb: rgba(255,255,255,0.22); --thumb-hover: rgba(255,255,255,0.4); --input-bg: rgba(255,255,255,0.06); font-family: "Source Han Sans CN","Source Han Sans SC","Microsoft YaHei","PingFang SC",sans-serif; font-weight: 700; color: var(--text); letter-spacing: 0; -webkit-font-smoothing: antialiased; } /* ===== 浅色变量 ===== */ .ark.light { --bg-panel: linear-gradient(180deg, rgba(250,251,252,0.97) 0%, rgba(233,237,241,0.97) 100%); --fab-bg: linear-gradient(180deg, rgba(250,251,252,0.98), rgba(234,238,242,0.98)); --text: #111213; --muted: #5b6066; --muted-2: #6b7280; --border: rgba(0,0,0,0.24); --border-soft: rgba(0,0,0,0.12); --border-faint: rgba(0,0,0,0.06); --layer: rgba(0,0,0,0.05); --layer-hover: rgba(0,0,0,0.08); --chip-bg: rgba(0,0,0,0.04); --chip-hover: rgba(0,0,0,0.08); --accent-bg: #111213; --accent-text: #f5f7f9; --accent-hover: #2c3238; --danger: #e0382f; --info: #2f6fbf; --corner: rgba(0,0,0,0.75); --watermark: rgba(0,0,0,0.05); --shadow: 0 0 24px rgba(0,0,0,0.18); --thumb: rgba(0,0,0,0.22); --thumb-hover: rgba(0,0,0,0.45); --input-bg: rgba(255,255,255,0.6); } /* ===== 自动:跟随系统浅色 ===== */ @media (prefers-color-scheme: light) { .ark:not(.light):not(.dark) { --bg-panel: linear-gradient(180deg, rgba(250,251,252,0.97) 0%, rgba(233,237,241,0.97) 100%); --fab-bg: linear-gradient(180deg, rgba(250,251,252,0.98), rgba(234,238,242,0.98)); --text: #111213; --muted: #5b6066; --muted-2: #6b7280; --border: rgba(0,0,0,0.24); --border-soft: rgba(0,0,0,0.12); --border-faint: rgba(0,0,0,0.06); --layer: rgba(0,0,0,0.05); --layer-hover: rgba(0,0,0,0.08); --chip-bg: rgba(0,0,0,0.04); --chip-hover: rgba(0,0,0,0.08); --accent-bg: #111213; --accent-text: #f5f7f9; --accent-hover: #2c3238; --danger: #e0382f; --info: #2f6fbf; --corner: rgba(0,0,0,0.75); --watermark: rgba(0,0,0,0.05); --shadow: 0 0 24px rgba(0,0,0,0.18); --thumb: rgba(0,0,0,0.22); --thumb-hover: rgba(0,0,0,0.45); --input-bg: rgba(255,255,255,0.6); } } .fab { position: fixed; right: 24px; bottom: 24px; z-index: 2147483647; width: 64px; height: 40px; display: flex; align-items: center; justify-content: center; background: var(--fab-bg); border: 1px solid var(--border); box-shadow: var(--shadow); cursor: pointer; user-select: none; transition: none; } .fab:hover { border-color: var(--border); opacity: 0.92; } .fab:active { transform: scale(0.92); } .fab.dragging { cursor: grabbing; } .fab .fab-label { font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--text); } .fab-corner, .panel-corner { position: absolute; width: 7px; height: 7px; } .fab-corner.tl { top: 0; left: 0; border-top: 2px solid var(--corner); border-left: 2px solid var(--corner); } .fab-corner.tr { top: 0; right: 0; border-top: 2px solid var(--corner); border-right: 2px solid var(--corner); } .fab-corner.br { bottom: 0; right: 0; border-bottom: 2px solid var(--corner); border-right: 2px solid var(--corner); } .fab-corner.bl { bottom: 0; left: 0; border-bottom: 2px solid var(--corner); border-left: 2px solid var(--corner); } .panel { position: fixed; right: 24px; bottom: 72px; z-index: 2147483646; width: 344px; max-height: calc(100vh - 120px); display: flex; flex-direction: column; background: var(--bg-panel); border: 1px solid var(--border); box-shadow: var(--shadow); border-radius: 0; } .panel-corner { border: 1px solid var(--corner); } .panel-corner.tl { top: 0; left: 0; border-top-width: 2px; border-left-width: 2px; } .panel-corner.tr { top: 0; right: 0; border-top-width: 2px; border-right-width: 2px; } .panel-corner.br { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; } .panel-corner.bl { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; } .watermark { position: absolute; top: 52px; right: 14px; z-index: 0; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 52px; font-weight: 800; letter-spacing: 0; color: var(--watermark); pointer-events: none; } .header { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; cursor: move; touch-action: none; border-bottom: 1px solid var(--border-soft); } .label-strip { display: inline-block; background: var(--accent-bg); color: var(--accent-text); padding: 3px 10px; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 12px; letter-spacing: 2px; text-transform: uppercase; } .icon-btn { width: 24px; height: 24px; border: 1px solid var(--border); background: transparent; color: var(--text); cursor: pointer; transition: 0.2s; font-size: 12px; line-height: 1; display: flex; align-items: center; justify-content: center; } .icon-btn:hover { background: var(--layer); } .icon-btn:active { transform: scale(0.9); } .tabs { display: flex; border-bottom: 1px solid var(--border-soft); } .tab { flex: 1; height: 36px; background: transparent; border: none; border-right: 1px solid var(--border-faint); color: var(--muted); cursor: pointer; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; letter-spacing: 1px; transition: 0.2s; } .tab:last-child { border-right: none; } .tab:active { transform: scale(0.97); } .tab.active { background: var(--accent-bg); color: var(--accent-text); } .content { flex: 1; overflow-y: auto; min-height: 120px; max-height: 380px; } .content::-webkit-scrollbar { width: 8px; } .content::-webkit-scrollbar-thumb { background: var(--thumb); } .content::-webkit-scrollbar-thumb:hover { background: var(--thumb-hover); } .tab-page { position: relative; z-index: 1; } .page-label { display: flex; justify-content: space-between; align-items: center; padding: 12px 12px 6px; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); } .page-label.sub { padding-top: 16px; } .page-label .count { font-family: "Bahnschrift",sans-serif; font-size: 11px; color: var(--info); } .tags { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 12px 8px; } .tag { display: inline-flex; align-items: center; gap: 5px; padding: 4px 6px 4px 9px; background: var(--layer); border: 1px solid var(--border); font-size: 12px; max-width: 220px; transition: 0.2s; } .tag:hover { border-color: var(--text); opacity: 0.95; } .tag .tag-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tag .tag-del { flex: none; width: 16px; height: 16px; border: none; background: transparent; color: var(--muted); cursor: pointer; font-size: 11px; line-height: 1; display: flex; align-items: center; justify-content: center; transition: 0.2s; } .tag .tag-del:hover { color: var(--danger); } .tag .tag-del:active { transform: scale(0.85); } .input-row { display: flex; gap: 8px; padding: 4px 12px 14px; } .input-row input { flex: 1; height: 32px; padding: 0 10px; background: var(--input-bg); border: 1px solid var(--border); color: var(--text); outline: none; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; transition: 0.2s; } .input-row input:focus { border-color: var(--text); } .input-row input::placeholder { color: var(--muted-2); font-weight: 400; } .btn-add { height: 32px; padding: 0 14px; background: var(--accent-bg); color: var(--accent-text); border: none; cursor: pointer; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; transition: 0.2s; } .btn-add:hover { background: var(--accent-hover); } .btn-add:active { transform: scale(0.95); } .level-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; padding: 4px 12px 8px; } .level-chip { height: 32px; display: flex; align-items: center; justify-content: center; background: var(--chip-bg); border: 1px solid var(--border); color: var(--muted); cursor: pointer; font-family: "Bahnschrift","Segoe UI",sans-serif; font-size: 12px; letter-spacing: 0.5px; transition: 0.2s; } .level-chip:hover { background: var(--chip-hover); } .level-chip:active { transform: scale(0.93); } .level-chip.active { background: var(--accent-bg); color: var(--accent-text); border-color: var(--accent-bg); } .hint { padding: 0 12px 14px; font-size: 11px; color: var(--muted-2); font-weight: 400; } .opt-list { display: flex; flex-direction: column; gap: 4px; padding: 4px 12px 12px; } .opt-row { display: flex; justify-content: space-between; align-items: center; height: 40px; padding: 0 10px; border: 1px solid var(--border-faint); cursor: pointer; transition: 0.2s; user-select: none; } .opt-row:hover { background: var(--layer-hover); } .opt-label { font-size: 12px; } .toggle { flex: none; width: 36px; height: 18px; border: 1px solid var(--border); background: var(--layer); position: relative; cursor: pointer; transition: 0.2s; } .toggle .knob { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; background: var(--muted); transition: 0.2s; } .toggle.on { background: var(--accent-bg); border-color: var(--accent-bg); } .toggle.on .knob { left: 20px; background: var(--accent-text); } .theme-grid { display: flex; gap: 6px; padding: 4px 12px 12px; } .theme-chip { flex: 1; height: 30px; display: flex; align-items: center; justify-content: center; background: var(--chip-bg); border: 1px solid var(--border); color: var(--muted); cursor: pointer; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; letter-spacing: 1px; transition: 0.2s; } .theme-chip:hover { background: var(--chip-hover); } .theme-chip:active { transform: scale(0.95); } .theme-chip.active { background: var(--accent-bg); color: var(--accent-text); border-color: var(--accent-bg); } .backup-actions { display: flex; gap: 6px; padding: 4px 12px 12px; } .backup-actions .btn-reset { flex: 1; height: 30px; font-size: 11px; } .reset-row { padding: 4px 12px 14px; } .btn-reset { width: 100%; height: 34px; background: transparent; border: 1px solid var(--border); color: var(--text); cursor: pointer; font-family: "Source Han Sans CN","Microsoft YaHei",sans-serif; font-weight: 700; font-size: 12px; letter-spacing: 1px; transition: 0.2s; } .btn-reset:hover { color: var(--danger); border-color: var(--danger); } .btn-reset:active { transform: scale(0.95); } .footer { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-top: 1px solid var(--border-soft); } .status { font-size: 11px; color: var(--muted); letter-spacing: 1px; } .ver { font-family: "Bahnschrift",sans-serif; font-size: 10px; color: var(--muted-2); letter-spacing: 1px; } `; const UI_HTML = `
FILTER
`; const TAB_PAGES = { keyword: 'pg-keyword', regex: 'pg-regex', user: 'pg-user', emoji: 'pg-emoji', level: 'pg-level', option: 'pg-option' }; const TAGGED_CATS = ['banMap', 'banRegex', 'banUser', 'banUid', 'banEmoji', 'banAt']; const OPT_ITEMS = [ { key: 'enabled', label: '启用评论过滤' }, { key: 'banExpand', label: '屏蔽全部笔记' }, { key: 'logMode', label: '调试 · 控制台日志' }, { key: 'redLine', label: '调试 · 红色标记不隐藏' }, { key: 'fabFollow', label: '悬浮按钮跟随滚动' } ]; let shadow = null; function flashStatus(msg) { const st = shadow.getElementById('status'); st.textContent = msg; clearTimeout(flashStatus._t); flashStatus._t = setTimeout(() => { st.textContent = 'AUTO SAVE'; }, 1200); } function applyFilter() { pendingRegexChecks.length = 0; // 配置变更后丢弃残留正则检测任务,防止旧规则误隐藏 resetFilterMarks(); runCore(); flashStatus('已应用'); } function renderTags(cat) { const box = shadow.getElementById('tags-' + cat); box.innerHTML = ''; for (const v of config[cat]) { const tag = document.createElement('span'); tag.className = 'tag'; const txt = document.createElement('span'); txt.className = 'tag-text'; txt.textContent = v; const del = document.createElement('button'); del.className = 'tag-del'; del.textContent = '✕'; del.title = '移除'; del.dataset.cat = cat; del.dataset.value = v; tag.appendChild(txt); tag.appendChild(del); box.appendChild(tag); } const count = shadow.querySelector(`.count[data-count="${cat}"]`); if (count) count.textContent = String(config[cat].length); } function renderLevels() { const box = shadow.getElementById('level-grid'); box.innerHTML = ''; for (const lv of LEVELS) { const chip = document.createElement('button'); chip.className = 'level-chip' + (config.banLevel.includes(lv) ? ' active' : ''); chip.textContent = lv === 'level_h' ? '6+' : lv.replace('level_', 'Lv.'); chip.title = lv; chip.dataset.level = lv; box.appendChild(chip); } } function renderOptions() { const list = shadow.getElementById('opt-list'); list.innerHTML = ''; for (const o of OPT_ITEMS) { const row = document.createElement('label'); row.className = 'opt-row'; const label = document.createElement('span'); label.className = 'opt-label'; label.textContent = o.label; const tg = document.createElement('span'); tg.className = 'toggle' + (config[o.key] ? ' on' : ''); tg.dataset.opt = o.key; const knob = document.createElement('span'); knob.className = 'knob'; tg.appendChild(knob); row.appendChild(label); row.appendChild(tg); list.appendChild(row); } } function renderAll() { TAGGED_CATS.forEach(renderTags); renderLevels(); renderOptions(); } function renderTheme() { const grid = shadow && shadow.getElementById('theme-grid'); if (!grid) return; grid.querySelectorAll('.theme-chip').forEach(c => { c.classList.toggle('active', c.dataset.theme === config.theme); }); } // 应用主题:auto 不加类(走 media 跟随系统),light/dark 强制加类 function applyTheme() { const ark = shadow && shadow.querySelector('.ark'); if (!ark) return; ark.classList.toggle('light', config.theme === 'light'); ark.classList.toggle('dark', config.theme === 'dark'); renderTheme(); } function setTheme(t) { if (t !== 'auto' && t !== 'light' && t !== 'dark') return; config.theme = t; applyTheme(); saveConfig(); flashStatus('已应用'); } // 导出:下载 JSON 备份文件 function downloadConfig() { try { const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bili_rebuild_config_' + new Date().toISOString().slice(0, 10) + '.json'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); flashStatus('已下载备份文件'); } catch (e) { flashStatus('导出失败'); } } // 导入:点击选择本地 JSON 文件后读取并应用 function importFile() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json,text/plain'; input.style.display = 'none'; document.body.appendChild(input); input.addEventListener('change', () => { const file = input.files && input.files[0]; if (!file) { input.remove(); return; } const reader = new FileReader(); reader.onload = () => { importConfig(String(reader.result || '')); }; reader.onerror = () => { flashStatus('读取文件失败'); }; reader.readAsText(file); input.remove(); }); input.click(); } // 导入:解析 JSON 并按白名单字段合并到配置 function importConfig(text) { const value = (text || '').trim(); if (!value) { flashStatus('内容为空'); return; } let obj; try { obj = JSON.parse(value); } catch (e) { flashStatus('JSON 解析失败'); return; } if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { flashStatus('格式不正确'); return; } config = sanitizeConfig(obj); regexBlacklist.clear(); saveConfig(); renderAll(); applyTheme(); applyFilter(); flashStatus('已导入并应用'); } function addItem(cat) { const input = shadow.getElementById('in-' + cat); const value = (input.value || '').trim(); if (!value) return; if (!config[cat].includes(value)) { if (cat === 'banRegex') { // 词库更新策略:正则入库前做危险模式预检 + 耗时基准验证(危险模式跳过基准,防同步卡死) const warns = []; const dangerous = DANGEROUS_REGEX.test(value); if (dangerous) warns.push('存在 ReDoS 风险'); const bench = dangerous ? 0 : testRegexSync(BENCH_TEXT, value); if (bench > 8) warns.push('耗时 ' + Math.round(bench) + 'ms 偏慢'); config[cat].push(value); renderTags(cat); saveConfig(); applyFilter(); flashStatus(warns.length ? ('已添加(' + warns.join(',') + ')') : '已添加'); } else { config[cat].push(value); renderTags(cat); saveConfig(); applyFilter(); flashStatus('已添加'); } } else { flashStatus('已存在'); } input.value = ''; input.focus(); } function removeItem(cat, value) { config[cat] = config[cat].filter(v => v !== value); if (cat === 'banRegex') regexBlacklist.clear(); // 词库变更后重置超时黑名单 renderTags(cat); saveConfig(); applyFilter(); } function toggleLevel(lv) { const arr = config.banLevel; config.banLevel = arr.includes(lv) ? arr.filter(x => x !== lv) : [...arr, lv]; renderLevels(); saveConfig(); applyFilter(); } function toggleOption(key) { if (key === 'fabFollow') { const fab = shadow.getElementById('fab'); const vtop = fab ? Math.round(fab.getBoundingClientRect().top) : null; if (config.fabFollow) { // 跟随屏幕 → 钉在网页:当前视口位置转成文档位置 config.fabDocY = (config.fabTop ?? (vtop ?? 0)) + window.scrollY; } else { // 钉在网页 → 跟随屏幕:当前文档位置转成视口位置 config.fabTop = (config.fabDocY ?? (vtop ?? 0) + window.scrollY) - window.scrollY; } config.fabFollow = !config.fabFollow; renderOptions(); saveConfig(); positionFab(); flashStatus('已应用'); return; } config[key] = !config[key]; renderOptions(); saveConfig(); applyFilter(); } function resetConfig() { config = deepClone(DEFAULT_CONFIG); regexBlacklist.clear(); saveConfig(); renderAll(); applyTheme(); applyFilter(); flashStatus('已重置'); } // 定位悬浮按钮:开启跟随屏幕=用视口坐标(滚动时不变);关闭钉在网页=文档坐标-滚动偏移 function positionFab() { const fab = shadow && shadow.getElementById('fab'); if (!fab) return; const hasCustom = config.fabX != null && (config.fabTop != null || config.fabDocY != null); if (!hasCustom) return; // 未自定义时保持 CSS 默认右下角 fab.style.right = 'auto'; fab.style.bottom = 'auto'; fab.style.left = config.fabX + 'px'; const vtop = config.fabFollow ? config.fabTop : config.fabDocY - window.scrollY; fab.style.top = vtop + 'px'; } function initUI() { const host = document.createElement('div'); host.id = 'bili-rebuild-host'; shadow = host.attachShadow({ mode: 'open' }); shadow.innerHTML = `${UI_HTML}`; (document.body || document.documentElement).appendChild(host); const panel = shadow.getElementById('panel'); const fab = shadow.getElementById('fab'); if (!SHOW_FAB) fab.hidden = true; const closeBtn = shadow.getElementById('closeBtn'); const header = shadow.getElementById('header'); const content = shadow.getElementById('content'); // 悬浮按钮:拖拽定位 + 点击开关面板(位移 > 4px 视为拖拽,否则为点击) let fabDrag = false, fabMoved = false, fx0 = 0, fy0 = 0, fdx = 0, fdy = 0; fab.addEventListener('pointerdown', (e) => { fabDrag = true; fabMoved = false; fx0 = e.clientX; fy0 = e.clientY; const r = fab.getBoundingClientRect(); fdx = e.clientX - r.left; fdy = e.clientY - r.top; fab.classList.add('dragging'); fab.setPointerCapture(e.pointerId); }); fab.addEventListener('pointermove', (e) => { if (!fabDrag) return; if (!fabMoved && Math.hypot(e.clientX - fx0, e.clientY - fy0) > 4) fabMoved = true; if (!fabMoved) return; const x = Math.max(0, Math.min(e.clientX - fdx, window.innerWidth - 64)); const y = Math.max(0, Math.min(e.clientY - fdy, window.innerHeight - 40)); fab.style.right = 'auto'; fab.style.bottom = 'auto'; fab.style.left = x + 'px'; fab.style.top = y + 'px'; }); fab.addEventListener('pointerup', () => { if (!fabDrag) return; fabDrag = false; fab.classList.remove('dragging'); if (fabMoved) { const r = fab.getBoundingClientRect(); config.fabX = Math.round(r.left); const vtop = Math.round(r.top); if (config.fabFollow) config.fabTop = vtop; else config.fabDocY = vtop + window.scrollY; saveConfig(); positionFab(); flashStatus('位置已保存'); } else { panel.hidden = !panel.hidden; } }); fab.addEventListener('pointercancel', () => { fabDrag = false; fab.classList.remove('dragging'); }); // 滚动时同步按钮位置(钉在网页模式下随内容移动) window.addEventListener('scroll', positionFab, { passive: true }); closeBtn.addEventListener('click', () => { panel.hidden = true; }); // Tab 切换 shadow.querySelector('.tabs').addEventListener('click', (e) => { const btn = e.target.closest('.tab'); if (!btn) return; shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t === btn)); shadow.querySelectorAll('.tab-page').forEach(p => { p.hidden = true; }); shadow.getElementById(TAB_PAGES[btn.dataset.tab]).hidden = false; const focusId = { keyword: 'in-banMap', regex: 'in-banRegex', user: 'in-banUser', emoji: 'in-banEmoji' }[btn.dataset.tab]; if (focusId) { const f = shadow.getElementById(focusId); if (f) f.focus(); } }); // 面板内点击委托 content.addEventListener('click', (e) => { const del = e.target.closest('.tag-del'); if (del) { removeItem(del.dataset.cat, del.dataset.value); return; } const addBtn = e.target.closest('[data-add]'); if (addBtn) { addItem(addBtn.dataset.add); return; } const chip = e.target.closest('.level-chip'); if (chip) { toggleLevel(chip.dataset.level); return; } const tg = e.target.closest('.toggle'); if (tg) { toggleOption(tg.dataset.opt); return; } const themeChip = e.target.closest('.theme-chip'); if (themeChip) { setTheme(themeChip.dataset.theme); return; } const exportFile = e.target.closest('#exportFileBtn'); if (exportFile) { downloadConfig(); return; } const importFileBtn = e.target.closest('#importFileBtn'); if (importFileBtn) { importFile(); return; } const reset = e.target.closest('#resetBtn'); if (reset) { resetConfig(); return; } }); // 输入框回车添加 content.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; const input = e.target.closest('input[id^="in-"]'); if (input) { addItem(input.id.replace('in-', '')); } }); // 拖拽面板(标题栏) let dragging = false, dx0 = 0, dy0 = 0; header.addEventListener('pointerdown', (e) => { if (e.target.closest('.icon-btn')) return; dragging = true; const r = panel.getBoundingClientRect(); dx0 = e.clientX - r.left; dy0 = e.clientY - r.top; header.setPointerCapture(e.pointerId); }); header.addEventListener('pointermove', (e) => { if (!dragging) return; panel.style.left = Math.max(0, Math.min(e.clientX - dx0, window.innerWidth - 90)) + 'px'; panel.style.top = Math.max(0, Math.min(e.clientY - dy0, window.innerHeight - 90)) + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); header.addEventListener('pointerup', () => { dragging = false; }); header.addEventListener('pointercancel', () => { dragging = false; }); } // 启动 UI(渲染配置 + 注入面板) function boot() { initUI(); renderAll(); applyTheme(); positionFab(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();