这是一个针对 B 站“消息/通知清理”场景的油猴脚本项目。文章会围绕油猴脚本的安装、功能设计、免登录思路、多端同步逻辑、代码实现要点、自动化验证与排错展开。下文按技术手册方式组织,便于直接照着操作。
开头
这次聊的油猴插件,目标很明确:一键清理 B 站网页端的未读消息和红点提示,作用范围是浏览器里的消息中心、动态提醒这类场景。插件的核心卖点是 v0.2 版本开始支持“多端免登录”,不需要在每个浏览器里重新扫码绑定账号,装好脚本、完成一次初始化后,其余设备直接沿用浏览器自身的登录态,脚本只做页面内清理操作,不额外收集账号信息。
很多油猴用户都有这种体验:B 站网页版打开以后,通知铃铛上总是挂着未读数字。有些是旧动态、旧回复,有些是系统通知,真正重要的消息往往被埋在前面。手动点开“查看所有消息”一条条处理太慢,全选又没有单独的“全部已读”快捷键。这个插件就是解决了这个“批量置为已读”的需求。v0.2 里主要增加了跨浏览器设备复用配置、清理范围分类、自动触发和手动批量清理四种模式。
本文会按安装部署、功能测试、代码结构、多端免登录机制、批量任务设计、常见问题这几块展开。手里有油猴扩展、能打开 B 站网页版的用户都可以照着走一遍,整个验证流程不依赖后端服务,也不需要购买服务器。
1. 核心能力速览
先给出一张总表,方便判断这个工具是否符合你的场景。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 浏览器端油猴脚本(UserScript) |
| 适用平台 | Windows / macOS / Linux 上支持 Tampermonkey、Violentmonkey 的浏览器 |
| 目标站点 | B 站网页版消息中心、动态未读、私信列表 |
| 主要功能 | 一键标记消息已读、按类型清理、自动清理开关、自定义过滤条件 |
| 多端免登录 | v0.2 核心改进,配置存在浏览器本地,不额外绑定账号 |
| 安装方式 | 油猴扩展内安装脚本文件,或本地开发者模式加载 |
| API 支持 | 无独立 HTTP API,但提供脚本内可调用函数,方便二次开发 |
| 批量任务 | 支持遍历消息列表批量处理,可限制单次清理条数 |
| 建议运行环境 | 最新版 Chrome / Edge / Firefox,开启油猴扩展 |
| 依赖项 | 无 Python、无 Node.js,不需要本地服务 |
这里需要提醒的是:表格里体现的是普通油猴脚本能做到的事情。具体消息接口结构和页面 DOM 随时会随 B 站改版变化,如果打开页面后发现功能失效,优先检查脚本更新而不是先怀疑浏览器。
1.1 免登录是什么意思
很多用户看到“多端免登录”会误以为脚本内置了绕过登录的手段,实际上不是。
这个脚本运行在已经登录 B 站的浏览器标签页里,它使用的是当前页面已有的登录态。所谓“免登录”,指的是在不同电脑上使用同一份脚本配置时,不需要再去绑定设备、不需要输验证码、不需要扫码授权,因为脚本从未依赖单独的第三方会话。它只通过浏览器自身保存的登录态读取当前用户的消息列表,并在页面 DOM 上触发“已读”操作。
这种设计的两个好处是:
- 不需要在油猴脚本里保存任何账号密码,降低了泄露风险。
- 换设备后只要浏览器登录状态正常,脚本就能工作。
需要注意的边界是:脚本不能在未登录状态下读取服务端消息,也不能在没有登录态的浏览器里“凭空”清理账号消息。所谓免登录,是免除“给脚本单独授权”这个步骤。
1.2 清理能力边界
v0.2 适合处理的是网页端可见的、用户自己账号下的消息。例如:
- 回复我的内容
- @ 我的内容
- 收到的赞
- 系统通知
- 私信列表已读标记
它不会做的事情包括:删除他人评论、批量拉黑用户、修改账号设置、抓取非公开数据。如果后续有类似“自动删除历史评论”的需求,必须在合法的用户操作流程内重新设计,不能简单通过模拟点击绕过确认框。任何涉及账号内容的自动化操作,都要保证操作可撤销、可审计、在用户本人授权范围内进行。
2. 适用场景与使用边界
2.1 谁适合用
如果你经常同时开着 B 站首页、动态页、私信页,但又不想一直点掉红点,这个脚本就很合适。它适合以下人群:
- 每天需要频繁打开 B 站网页版处理消息的运营人员。
- 管着多个浏览器环境,希望一键同步清理配置的测试人员。
- 不习惯被未读数字干扰,想保持消息列表干净的内容创作者。
- 准备用油猴脚本做浏览器自动化练习的开发者。
以运营场景为例,一个账号如果每天新增几百条互动消息,纯手工点已读会消耗大量时间。脚本可以将“点开消息中心、逐条进入会话、找到已读按钮、返回列表”这一套重复流程压缩成一次点击,把操作时间从几分钟降低到十几秒。当然具体时间取决于网络状态和列表长度。
2.2 边界和不适合的场景
不适合的场景是:
- 期望清理后,服务端页面上所有历史消息全部消失——脚本通常只标记已读,不删除远端数据。
- 需要跨账号批量处理——油猴脚本一般运行在单账号登录上下文内,不支持并行处理多个账号。
- 需要后台定时清理而且电脑经常关机——油猴脚本受浏览器生命周期约束,浏览器没打开就没有执行时机。
- 需要清理移动 App 内消息——脚本跑在网页端,无法控制 App。
2.3 合规和隐私提醒
这里要特别强调:即使脚本目标是“清理自己的消息”,也必须注意不要做以下事情:
- 不要伪造他人身份执行操作。
- 不要使用脚本绕过 B 站的验证码或风控机制。
- 不要通过脚本抓取其他用户非公开数据。
- 不要在公共电脑上长期开启“自动清理”,避免操作到不该处理的会话。
如果要把脚本分享给团队或发布到社区,建议在说明里写清楚这是一个“当前账号网页端已读辅助工具”,并提示用户自行熟悉清理操作产生的不可逆影响。涉及消息记录、私信等敏感内容时,发布前做效果复核,避免误点删除。
3. 环境准备与前置条件
3.1 浏览器和油猴扩展
这个脚本依赖油猴扩展运行,目前主流选择是:
- Tampermonkey:兼容性好,支持 Chrome/Edge/Firefox 等多个浏览器
- Violentmonkey:开源版本,界面清爽,适合注重审查的开发者
我不建议用已经停止维护的旧版 Greasemonkey 测试新脚本。虽然 Greasemonkey 对旧脚本兼容性不错,但 B 站页面已经大量使用现代 JavaScript,旧脚本引擎可能无法正确处理页面动态渲染。
建议使用较新版本浏览器。以 Chrome/Edge 浏览器为例,如果浏览器版本过老,可能不支持MutationObserver、async/await等特性,会导致脚本解析失败。
3.2 浏览器的开发者模式
在本地安装脚本文件时,不需要开启开发者模式。但如果要做脚本调试,则需要打开开发者工具,快捷键一般是 F12 或 Ctrl+Shift+I。
为了更稳定地观察页面节点变化,建议在开发者工具里关闭缓存。具体方法是:打开开发者工具 → Network 选项卡 → 勾选 Disable cache。
3.3 网络和登录环境要求
使用脚本前请确认以下条件:
- 浏览器能正常访问 B 站网页版。
- 当前账号已经登录。
- 需要清理的消息类型在网页端可见。
- 如果页面使用了旧版消息入口,脚本可能需要适配不同 URL。
由于浏览器插件的持久化存储容量有限,一般不建议在脚本内保存大量日志。v0.2 把配置项限制在几 KB 以内,绝大多数浏览器都能稳定保存。
4. 安装部署与启动方式
4.1 在线安装脚本
如果脚本已经发布到 Greasy Fork 或类似脚本市场,安装流程如下:
- 打开 Greasy Fork 页面,搜索脚本名称。
- 点击“安装此脚本”。
- 油猴扩展弹出安装确认页面。
- 点击“安装”。
- 打开 B 站消息中心页面,确认脚本是否注入。
安装完成后,油猴扩展图标上会出现角标数字,表示当前页面有脚本运行。如果图标没有角标,可能是脚本的匹配规则没有覆盖当前页面。
一个规范的脚本元数据会包括名称、命名空间、匹配 URL、版本号、授权信息。以下是一个入门模板:
// ==UserScript== // @name B站消息清理助手 // @namespace https://example.com/bilibili-cleaner // @version 0.2.0 // @description 清理B站网页端未读消息,支持多端浏览器本地配置同步 // @author YourName // @match https://www.bilibili.com/* // @match https://message.bilibili.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // ==/UserScript==注意:@namespace需要替换成你自己的域名或项目地址,@match需要按实际站点地址调整,不能照抄。GM_setValue和GM_getValue是油猴扩展的存储 API,用于保存配置。如果脚本不需要在这些扩展之间移植,可以不加。
4.2 本地打包加载
对于开发版本,通常会选择本地加载,因为每次修改后可以直接在管理器里点“重新加载”,不需要重复安装。
本地加载有两种方式:
- 直接把
.user.js文件拖进油猴扩展管理面板。 - 在 Tampermonkey 管理面板点击“添加新脚本”,把代码整体粘贴进去。
推荐第二种方式,便于在粘贴前顺手修改元数据。粘贴后点击 Ctrl+S 保存,脚本会立即生效。刷新 B 站页面后即可看到效果。
如果不是在 Greasy Fork 安装,浏览器可能提示“扩展程序需要审查”,这是正常现象。需要确认油猴扩展的安装来源是官方商店,避免下载到修改过的恶意扩展。
4.3 多端部署的两种方案
多端部署指的是在办公室电脑、家用电脑等多个浏览器环境加载同一份脚本配置。重点在“配置同步”,而不是“消息状态云端同步”。
方案一:手动导出导入配置
油猴扩展本身不提供脚本配置的云同步后端,但脚本可以在菜单里提供“导出配置”和“导入配置”选项。操作方式是:
- 在 A 电脑上点击油猴扩展菜单中的“导出配置”。
- 浏览器会下载一个 JSON 文件。
- 将文件通过内部网盘或本地存储发送到 B 电脑。
- 在 B 电脑上点击“导入配置”。
- 脚本读取 JSON 并写入本地存储。
方案二:使用脚本编辑器的同步功能
Tampermonkey 在开启同步后,可以把脚本本体和设置同步到浏览器账号。此方式适合同品牌浏览器环境,但不同浏览器之间仍然无法直接同步。如果只是希望脚本代码一致,用 Git 仓库保存脚本源码是更稳妥的办法。通过在多个设备上安装同一份脚本源文件,功能行为就能保持一致。
这里“免登录”的真正价值在于:脚本配置里不需要包含用户身份信息。同步的只是清理规则、过滤关键词、启用状态、页面选择器配置,不包含登录令牌。因此配置文件泄露不会直接导致账号被登录,可以显著降低同步风险。
4.4 启动和关闭
脚本不需要单独启动服务。打开 B 站网页版,油猴扩展会自动注入脚本。如果页面已经打开,脚本更新后需要手动刷新页面生效。
如果想临时关闭,点击扩展图标,在弹出的菜单里关闭对应脚本即可。不要直接卸载脚本,卸载后本地保存的配置不会自动清理,需要在浏览器开发者工具里手动清除对应站点的本地存储。
5. 功能测试与效果验证
5.1 测试前的准备工作
测试前准备一个干净的测试页面:
- 打开 B 站消息中心页。
- 确认登录状态正常。
- 确认至少存在 2 条以上未读消息。
- 打开浏览器控制台,输入
console.log(document.readyState),返回complete表示页面加载完成。
测试过程中建议不要同时开启其他 B 站自动化脚本,避免多个脚本同时操作 DOM 导致节点定位失败。
5.2 一键清理测试
一键清理是最基础的功能。点击插件菜单中的“清理全部消息”后,脚本会遍历当前页面的消息列表,对每条消息执行“标记已读”操作。
判断成功的标准:
- 页面未读数字清零。
- 消息项的未读标识样式消失。
- 控制台没有报错。
如果第一次点击后未读数字没有变化,优先检查当前页面是否处于“全部消息”Tab。B 站消息中心有多种子页面,脚本默认只处理消息列表页 DOM。如果停留在“系统通知”页,清理函数可能找不到目标节点。
此时可以在控制台执行以下脚本,查看脚本是否已经注入:
window.__biliCleaner !== undefined如果返回undefined,说明脚本没有在当前页面运行,需要检查匹配规则。
5.3 按类型清理测试
v0.2 里可以按消息类型清理,比如只清理“收到的赞”,保留“私信”。测试时先设置过滤条件,再执行清理。
操作步骤:
- 点击脚本菜单,进入设置面板。
- 勾选需要清理的消息类型。
- 点击保存。
- 切到对应 Tab。
- 触发清理。
在开发过程中,可以通过观察 DOM 来确认消息类型。B 站通知列表的每条消息通常会包含类型图标或分组标题,脚本需要用文本匹配或 CSS 类名过滤。由于 B 站前端经常改动类名,建议在代码里同时支持两种定位方式:
- 文本关键词匹配。
- >function getMessageType(node) { const text = node.innerText || ''; if (text.includes('赞')) { return 'like'; } if (text.includes('回复')) { return 'reply'; } return 'unknown'; }
这里的
getMessageType只是一个辅助函数,实际项目中还需要补充节点范围限制,避免把页面其他区域的文本误判为消息类型。5.4 自动清理测试
自动清理功能会在页面加载完成后,等待一段时间再触发清理动作。这样做的原因是 B 站页面内容可能是异步渲染,如果脚本在 DOM 还没生成时就执行,会找不到目标节点。
测试时需要验证以下场景:
- 正常进入消息中心,页面加载后自动触发。
- 首次进入页面时禁用自动清理,手动点击时才触发。
- 消息列表为空时不触发额外动作。
- 脚本循环执行时不会产生无限循环。
自动清理的标准实现可以这样写:
function waitForList(timeout = 8000) { return new Promise((resolve) => { const startedAt = Date.now(); const timer = setInterval(() => { const list = document.querySelector('.message-list'); if (list || Date.now() - startedAt > timeout) { clearInterval(timer); resolve(list); } }, 500); }); }waitForList会每 500ms 检查一次消息列表节点是否存在。超时后仍然 resolve,因此调用方必须判断 list 是否为空。自动清理测试是否成功的判断标准是:刷新页面后 3 到 5 秒内,页面消息未读数从 N 变成 0。如果超过 10 秒仍然有未读数,可能是选择器已失效,也可能是网络请求延迟。
5.5 清理条数限制测试
批量处理消息时有一个潜在风险:如果当前未读消息过多,脚本一次性对几百条节点连续操作,可能造成页面卡顿,也可能触发风控限制。
v0.2 建议在配置里加入“单次最大清理条数”。默认值可以设为 50,测试时先设置成 3,观察脚本是否只清理前 3 条。
实现逻辑大致是:
function cleanMessages(maxCount = 50) { const items = getMessageItems(); const targets = items.filter(isUnread).slice(0, maxCount); targets.forEach(markAsRead); }这里
maxCount需要结合列表滚动位置调整。如果列表只加载了前 20 条节点,即使设置 100 条也无法处理后续数据。如果要处理超过首屏加载数量的消息,需要加入滚动加载机制。比较稳妥的思路是:滚动到底部 → 等待列表长度增加 → 再次清理 → 直到没有新增项。
5.6 网络请求模式验证
油猴脚本有时会通过
GM_xmlhttpRequest直接请求站点接口,而不是模拟点击 DOM。测试这种模式时要重点确认:- 请求是否需要额外鉴权参数。
- 接口返回的数据结构是否稳定。
- 是否需要在请求头里添加 Referer。
在开发者工具的 Network 面板中观察请求,如果请求返回 401 或 403,说明当前会话失效或缺少必要的请求头。此时不要强行绕过,正确的处理是提示用户重新登录。
6. 多端免登录机制与持久化设计
6.1 登录态来源
多端免登录的技术本质是“不新建登录态”。
很多脚本在处理网站消息时会设计独立的“登录扫码”流程,但这样会带来几个问题:令牌保存在脚本存储中容易泄露;刷新令牌逻辑复杂;更换设备后需要重新扫码。B 站清理助手 v0.2 放弃了独立授权,直接依赖浏览器当前站点 Cookie 和本地会话。
这里有一张简单对比:
设计方式 是否便于多端部署 安全风险 脚本内保存用户账号密码 低,多端需要重复录入 高 脚本申请独立 Token 中,需要刷新逻辑 中 依赖浏览器已有登录态 高,浏览器登录即用 低 采用第三种方式后,不同浏览器环境只要都登录了同一个账号,脚本就能正常读取该账号下的消息中心内容。所谓“免登录”正是省去了给脚本单独授权的环节。
不过要提醒一句:多端免登录不等于多端可在同一时刻并行操作。两个浏览器同时处理同一个账号的同一批消息时,服务端数据可能发生竞争,最终结果取决于服务器处理顺序。比如 A 浏览器把消息标记为已读,B 浏览器刷新后又看到已读状态,这都是正常现象。
6.2 本地配置存储
脚本的配置信息建议存储在油猴扩展提供的
GM_setValue/GM_getValue中,而不是完全依赖localStorage。原因是
localStorage是按域名隔离的,B 站页面的localStorage如果被站点脚本意外清理,会导致脚本配置丢失。GM_setValue存储在油猴扩展自己的数据目录中,容量稍微大一些,也不容易被页面脚本误删。保存配置的示例:
const DEFAULT_CONFIG = { autoClean: false, maxCleanCount: 50, filterLikes: true, filterReplies: true, filterSystem: false }; function saveConfig(cfg) { const merged = Object.assign({}, DEFAULT_CONFIG, cfg); GM_setValue('biliCleanerConfig', JSON.stringify(merged)); } function loadConfig() { const stored = GM_getValue('biliCleanerConfig', ''); if (!stored) { return Object.assign({}, DEFAULT_CONFIG); } try { return Object.assign({}, DEFAULT_CONFIG, JSON.parse(stored)); } catch (e) { return Object.assign({}, DEFAULT_CONFIG); } }通过合并默认配置,可以有效避免旧版本配置文件缺少新字段导致的错误。如果 JSON 解析失败,则回退到默认值。
注意:
GM_setValue存储的值不能是对象,只能是字符串或基础类型,因此代码里先用JSON.stringify转换。如果油猴扩展版本支持直接存储对象,也建议统一使用 JSON 字符串格式,便于兼容不同扩展。6.3 跨浏览器导入导出
为了让多端部署更顺畅,v0.2 在扩展菜单里加入了“导出配置 JSON”和“导入配置 JSON”两个入口。
菜单注册示例:
GM_registerMenuCommand('导出配置', () => { const cfg = loadConfig(); const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bili-cleaner-config.json'; a.click(); URL.revokeObjectURL(url); }); GM_registerMenuCommand('导入配置', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json'; input.onchange = (event) => { const file = event.target.files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = () => { try { const cfg = JSON.parse(reader.result); saveConfig(cfg); alert('导入成功'); } catch (error) { alert('配置文件格式错误'); } }; reader.readAsText(file); }; input.click(); });这段代码可以直接在油猴脚本里运行,但会用到
GM_registerMenuCommand,需要在元数据里声明授权。导入导出时要注意:脚本只导入配置,不导入任何账号身份数据。配置文件里不存在密码、Cookie、Token 等字段。6.4 多端同步的注意事项
不同浏览器对用户脚本的存储策略不同,在端与端之间复制配置时,需要注意以下几点:
- 如果 B 电脑上的浏览器版本过低,可能不支持
Blob下载,要改用油猴扩展的GM_download。 - 导入配置前应备份当前配置,避免误覆盖。
- 如果配置里包含自定义正则表达式,在不同系统上转义规则可能导致差异,建议统一使用 JSON 保存。
在团队内部协作时,可以把配置文件模板提交到代码仓库,再由各成员导入。为了标识配置版本,可以在配置文件里加一个
version字段,导入时检查版本号,避免旧配置覆盖新功能参数。7. 脚本的关键逻辑与代码示例
7.1 页面消息项的通用定位思路
B 站消息中心消息项结构通常是:外层列表容器 → 列表项元素 → 内部文本、按钮、链接。假设不确定最新 CSS 类名,建议采用“语义化定位”:优先通过容器中包含的文本或 aria-label 判断消息类型,再定位按钮。
下面用一个不依赖 B 站内部类名的示例说明:
function findUnreadItems(container) { const possibleItems = container.querySelectorAll('li, .list-item, [role="listitem"]'); const unreadItems = []; possibleItems.forEach((item) => { const style = window.getComputedStyle(item); if (style.display === 'none' || style.visibility === 'hidden') { return; } if (item.querySelector('.unread') || /未读/.test(item.getAttribute('aria-label'))) { unreadItems.push(item); } }); return unreadItems; }这里
querySelectorAll的选择器需要按页面实际结构调整。如果脚本运行时报 “container is null”,说明容器选择器没有匹配到任何元素。7.2 模拟点击已读按钮
标记已读最常见方式是找到未读消息对应的“已读”按钮,然后触发 click。触发 click 要区分普通按钮和带事件委托的按钮。如果页面 JavaScript 通过事件委托监听,直接调用
element.click()可能不触发。更稳妥的方式是派发 MouseEvent:
function triggerClick(el) { if (!el) { return; } const event = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); el.dispatchEvent(event); }通过
dispatchEvent让事件冒泡到上层监听器,可以兼容更多页面逻辑。注意:如果按钮上同时绑定了
pointerdown或pointerup事件,仅派发 click 可能不够。此时需要模拟完整的指针事件序列。但这个复杂度较高,实际测试中建议先检查页面是否支持键盘操作,比如按钮聚焦后按回车。7.3 控制台日志和进度提示
批量处理消息时,最好在脚本中输出简要日志,方便判断是否卡住:
function logProgress(current, total) { console.log(`[BiliCleaner] 已处理 ${current}/${total}`); }但不要把日志输出到页面上,避免影响页面原有元素。可以借助油猴 UI 在角落显示一个轻量状态条,状态条使用固定定位,Z-index 不宜过高。最稳定的是用原生 JavaScript 创建一个 div:
function createStatusBar() { const bar = document.createElement('div'); bar.id = 'bili-cleaner-status'; bar.style.cssText = 'position:fixed;z-index:99999;right:16px;bottom:16px;background:#00aeec;color:#fff;padding:8px 12px;border-radius:4px;font-size:12px;display:none;'; document.body.appendChild(bar); return bar; } function showStatus(text) { const bar = document.getElementById('bili-cleaner-status'); if (!bar) { return; } bar.textContent = text; bar.style.display = 'block'; }页面刷新后状态条会自动消失。如果脚本是异步处理列表,状态条可以帮助观察任务是否完成。
7.4 MutationObserver 自动响应新消息
B 站网页端可能在用户停留在页面时通过 WebSocket 推送新消息。如果希望在新消息出现时自动清理,可以使用 MutationObserver 监听列表容器:
function observeList(listContainer, callback) { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { callback(); break; } } }); observer.observe(listContainer, { childList: true, subtree: true }); return observer; }Observer 的缺点是可能会频繁触发,例如页面滚动时动态加载列表项也会触发回调。因此回调函数里要增加节流或防抖处理,避免在短时间内执行多次清理动作。
7.5 防重复执行锁
清理动作执行过程中,如果用户又手动点击了按钮,会同时发起多个清理任务,容易造成页面元素重复更新。可以在脚本中加入一个简单状态锁:
let cleaning = false; async function runCleanTask() { if (cleaning) { console.warn('[BiliCleaner] 清理任务正在执行中'); return; } cleaning = true; try { await doClean(); } finally { cleaning = false; } }cleaning标记只存在于当前页面运行时。如果用户刷新页面,标记会重置,这是合理的,因为旧页面的清理任务已经随页面销毁。如果要在任务执行期间阻止用户重复点击,可以在清理结束时再恢复按钮置灰状态。8. 接口 API 与批量任务设计
8.1 不提供独立 HTTP API
从 v0.2 的架构来看,这个脚本本身不提供外部 HTTP API。它是运行在浏览器里的用户脚本,无法单独作为服务被其他程序调用。
但脚本内部提供了可调用函数,例如:
window.biliCleaner = { runClean: runCleanTask, cleanAll: cleanMessages, exportConfig: exportConfig, importConfig: importConfig, getConfig: loadConfig, setConfig: saveConfig };如果把
window.biliCleaner暴露到页面全局,其他同页面运行的脚本也能调用这些函数。但要注意命名冲突,建议挂到一个不太可能被覆盖的命名空间,比如window.BiliCleanerApp。如果想提供跨进程 API,需要另写一个浏览器扩展或本地 Node 服务。比如可以写一个最小 HTTP 服务,接收 POST 请求后调用浏览器的 CDP 接口来触发油猴脚本函数。这种设计复杂度高,不推荐在没有明确需求的情况下实现。
8.2 批量任务的通用队列设计
虽然油猴脚本不一定需要后台任务队列,但如果要处理成百上千条消息,最好还是构建一个简单的任务队列。
一个可用队列结构如下:
class CleanTaskQueue { constructor(concurrency = 1) { this.concurrency = concurrency; this.queue = []; this.running = 0; } add(task) { this.queue.push(task); this.next(); } next() { while (this.running < this.concurrency && this.queue.length > 0) { const task = this.queue.shift(); this.running++; task() .catch((err) => console.error('[BiliCleaner] task failed', err)) .finally(() => { this.running--; this.next(); }); } } }使用并发数 1 是因为消息清理操作本身涉及 DOM 变更,并发执行多个任务可能造成页面状态混乱。设置成按顺序执行更稳妥。
8.3 批量任务失败重试策略
清理操作如果失败,可能是页面节点已被移除,这时重试没有意义。如果是网络请求失败,可以加入指数退避:
function retry(fn, times = 3, delay = 1000) { return fn().catch((error) => { if (times <= 1) { throw error; } return new Promise((resolve) => setTimeout(resolve, delay)) .then(() => retry(fn, times - 1, delay * 2)); }); }这里指数退避的延迟分别是 1s、2s、4s,适合页面请求类任务。如果清理操作是同步 DOM 操作,添加重试反而会拖慢速度,建议区分场景使用。
8.4 任务状态回显
为了保证批量任务可视化,可以把任务状态写到状态条或者控制台。比较实用的方式是记录两个数字:待处理数量和已完成数量。
function runTasks(items) { const total = items.length; let completed = 0; items.forEach((item) => { try { markAsRead(item); completed++; showStatus(`剩余 ${total - completed} 条`); } catch (e) { console.warn(e); } }); if (completed === total) { showStatus('清理完成'); } }这个版本的代码没有异步等待,对大量 DOM 操作可能导致页面长时间阻塞。如果消息数量在 50 条以内影响不大;如果超过 200 条,建议改成每隔一段时间处理一批,给浏览器 UI 留下响应时间。
8.5 API 调用示例模板
当脚本确实需要调用 B 站页面内部接口时,可以参考下面的通用模板。由于各接口参数不同,不能直接用于生产环境:
async function requestPageApi(apiPath, params = {}) { const query = new URLSearchParams(params).toString(); const url = `${apiPath}?${query}`; const resp = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Accept': 'application/json' } }); if (!resp.ok) { throw new Error(`HTTP ${resp.status}`); } return resp.json(); }这里的
credentials: 'include'用于携带当前页面 Cookie。但要注意,即使请求成功,响应内容是否可信还需要验证。不能因为在页面上下文就忽略返回值的校验。9. 资源占用与性能观察
9.1 如何观察脚本运行时资源占用
用户脚本本身占用的内存通常很小,比较关键的是页面消息列表渲染带来的内存开销。如果消息列表包含大量节点,浏览器会变卡。
观察方式:
- 打开开发者工具 → Performance 面板。
- 点击录制按钮。
- 手动触发一次清理任务。
- 停止录制。
- 查看主线程的 JavaScript 执行时间。
如果清理任务执行时间超过 500ms,说明脚本当前循环和 DOM 操作耗时较长,需要拆分任务。对用户脚本来说,单独看“脚本占用多少显存”不适用,因为脚本运行在浏览器渲染进程中,不涉及 GPU 显存计算。如果有人说某某油猴脚本“显存占用 7G”,那基本是在混淆概念。
9.2 CPU 与浏览器事件循环影响
频繁操作 DOM 会拉高 CPU 使用率。特别是使用 MutationObserver 时,如果回调里有较重的计算逻辑,可能导致页面滚动卡顿。建议在回调函数中减少高频操作,例如不要每次 mutation 都执行完整的列表扫描,而是用一个“脏标记”延迟到空闲时间执行:
let needClean = false; function onMutation() { if (needClean) { return; } needClean = true; requestIdleCallback(() => { try { runCleanTask(); } finally { needClean = false; } }, { timeout: 2000 }); }9.3 清理大量消息时的性能优化
一次性处理大量消息可以采用“分批清理法”。
- 每批处理 20 到 30 条。
- 每批之间等待 200ms。
- 每次处理前重新获取消息列表,避免操作已被移除的节点。
- 如果列表需要滚动加载,则优先滚动,暂停清理。
分批处理能明显降低页面无响应的概率。批与批之间用
setTimeout或requestAnimationFrame隔开即可。9.4 端口占用的扩展补充
油猴脚本一般不监听端口,所以不会出现端口冲突问题。但是如果用浏览器开发者工具的远程调试接口来驱动脚本,比如配合 Selenium 或 Puppeteer,就需要注意 9222 远程调试端口是否被占用。如果遇到端口被占用,可以改成其他端口启动浏览器:
chrome --remote-debugging-port=9223这个命令只是通用调试参数,实际使用需要换成你自己系统里的 Chrome 可执行文件路径,避免路径不一致导致启动失败。
10. 常见问题与排查方法
问题现象 可能原因 排查方式 解决方案 油猴图标有角标但页面无按钮 @match 规则匹配但脚本执行时机不对 打开控制台查看脚本版本 把 @run-at 改为 document-idle,刷新页面 清理后未读数字不消失 页面异步更新消息列表,已读状态由服务端返回后更新 查看 Network 请求和响应 等待服务端响应再刷新页面,不要重复点击 部分消息被跳过 消息列表采用懒加载,未滚动加载的项不在 DOM 中 打开页面后先滚动到底部 加入滚动加载逻辑,或分多次清理 脚本设置了自动清理但未触发 B 站消息中心页面是单页应用,脚本只执行一次 观察 URL 是否由 hash 路由改变 监听 hashchange 和 popstate 事件 本地配置文件无法导入 文件内容不是合法 JSON,字段被截断 用格式化工具检查 JSON 修复文件后重新导入 更换电脑后配置不生效 插入脚本没被安装,或配置存储域名不同 对比两个环境的脚本版本和配置面板 确认两台设备使用相同脚本版本 菜单命令不显示 油猴扩展未授权 GM_registerMenuCommand 查看脚本配置里的 @grant 在 @grant 列表中加入相应权限 控制台出现 “Cannot read properties of null” DOM 节点选择器匹配失败 检查容器元素是否存在 更新选择器或增加等待时间 浏览器提示脚本来源不可信 安装渠道不是 Greasy Fork 官方页面 比较下载文件内容 尽量从可信来源安装 清理过程页面很卡 一次性处理过多 DOM 操作 打开 Performance 面板检查主线程耗时 分批处理,减少单次操作数 11. 最佳实践与使用建议
11.1 第一次先小参数验证
生产使用之前,先把“单次最大清理条数”设置成 5 条,确认所有功能正常后再调大。不要上来就清理几百条,尤其是在不确定页面节点结构是否匹配的情况下。
11.2 保留一份最小可运行配置
建议把脚本源码和一份最小配置模板保存在代码仓库中。最小配置模板只包含默认清理类型和开关,不包含任何个人信息。这样即使误操作了配置文件,也能从模板快速恢复。
11.3 消息清理要有日志和撤回意识
油猴脚本处理消息时,最好在本地保存一份操作日志,记录操作时间和类型。标记已读的操作一般是可逆的,但如果未来扩展出删除或隐藏评论功能,就必须给每个操作加“二次确认”和“操作记录”。
这里再次强调:用户脚本只能做浏览器端自动化,无法保证服务端数据完整可恢复。对不可逆操作,一定要保持谨慎。
11.4 保持脚本的可维护性
油猴脚本的生命周期往往很短,因为前端页面会改版。为了让脚本存活更久,建议独立封装“选择器配置”和“逻辑代码”。页面改版时只修改选择器配置,不重写整套逻辑。
例如:
const SELECTORS = { listContainer: '.message-list', messageItem: '.message-item', unreadMark: '.unread', clearButton: '.clear-all' };后续如果 B 站消息中心的类名更换,只需要更新
SELECTORS对象。这种写法的好处是脚本的其余逻辑不需要改动,排查问题也更方便。11.5 规范发布和声明
如果要在 Greasy Fork 等平台发布,建议在脚本简介里写清楚以下几点:
- 功能范围:仅辅助标记当前账号的网页端消息为已读。
- 已知限制:不支持 App 端,受浏览器运行状态影响。
- 兼容性:建议在 Tampermonkey 或 Violentmonkey 上运行。
- 隐私说明:不收集账号密码,不采集用户数据,不向第三方发送请求。
- 免责声明:页面结构和站点规则变化可能导致功能失效。
这样既方便用户判断,也降低了脚本被误用的风险。
12. 后续可以扩展的方向
如果对 v0.2 之后的功能有期待,几个可行的方向是:
12.1 规则引擎化
把“只清理赞、不清理私信、保留前三日系统通知”这类需求改成可视化规则配置。用户可以在弹窗中勾选条件和动作,脚本动态生成清理策略。
12.2 浏览器扩展化
油猴脚本的存储边界和跨浏览器能力有限。把它们迁移成 Manifest V3 浏览器扩展后,可以获得更多后台能力,比如定时清理、多标签页同步、自定义右键菜单。但开发复杂度也会相应增加。
12.3 支持移动端浏览器
Kiwi Browser 或 Firefox Android 上的油猴扩展可以尝试运行桌面版脚本,但页面结构和触摸事件差异较大。移动端适配需要单独调整选择器和按钮定位方式。
12.4 接入第三方通知中心
如果愿意把清理后的关键消息转发到私有通知服务,可以设计一个抽象层,脚本只负责输出清理结果,具体发送方式由外部配置决定。建议保留本地文件输出为 JSON 作为首选方式,降低服务依赖。
13. 总结
这个油猴脚本最值得尝试的点不是技术难度,而是把“点未读红点”这种高频重复操作彻底简化。v0.2 的多端免登录设计让脚本的部署成本大幅下降,对比传统独立授权方案更适合浏览器脚本生态。
第一次验证时,建议按照“单条清理 → 按类型清理 → 自动清理 → 批量清理”的顺序逐步进行。最容易踩的坑有两个:一是页面 DOM 选择器在 B 站改版后失效;二是懒加载消息列表导致只清理了首屏数据。测试时先在少量消息环境中确认逻辑,再扩大清理范围。不要忽略油猴扩展菜单的配置导入导出功能,多设备环境下它能节省大量重复配置时间。
如果需要长期稳定使用,最稳妥的做法是持续关注页面结构变化,并保留一个小参数测试环境。油猴脚本的性能开销不大,真正的成本在于页面适配维护。结合浏览器开发者工具和日志输出,多数问题都能在几分钟内定位。