
我有时候觉得B站网页端最磨人的不是找不到想看的视频而是右上角那个小红点。私信几条未读、提醒几条、系统通知又来了于是你点进去一条条处理回到列表再点下一条。如果只是偶尔一次还好天天都这样就会变成一种没办法忽略的重复劳动。最近看到一个小型油猴脚本项目标题是“B站消息清理助手 v0.2多端免登陆”。虽然这个项目的介绍文字很少但这个标题已经足够引起人兴趣。不是因为“清理消息”多神奇而是这里的 v0.2 这个版本号很有信息量。它意味着作者不是停在“能删消息”这一步而是往前走了一版处理了删得快、删得稳、换端也能用的问题。我更愿意把这类工具理解成一句话它真正提供的不是“一键删除”这个结果而是把重复操作变成可复用、可控制、可回看的小型工作流。本文不宣称抄一段代码就能直接跑也不替这个项目做保证只展开聊这类脚本从零到一、从一到 0.2 背后要面对的工程问题以及你安装一个陌生用户脚本之前到底该关心什么。另外先把一个细节说清楚原标题里的“免登陆”应该是“免登录”。“登陆”是登上海岛不是登录网页。下面的讨论都按“登录”来写。1. 先搞清楚它要处理的是哪一类B站消息以及你该不该用脚本1.1 B站的“消息”并不是同一种东西很多人一说消息清理脑子里只有“私信”。但B站的网页端里能产生红点和未读的入口其实很杂。常见的大致有这么几类私信会话和 UP 主、亲友、群聊之间的站内信通常有“进入会话”“删除会话”这类操作。提醒和回复你在评论区被 、被回复之后产生的提醒。系统通知账号安全、活动、创作中心、充电等各类通知。动态和私信里的单向消息有些消息只是通知性质并不具备“会话”概念。这些消息有一个关键差别有的是“标记已读”有的是“删除会话”。标记已读通常不可逆但至少不会把聊天记录直接弄没删除会话则完全不同点了就没有了大概率没有回收站。所以一个批量清理脚本要考虑的不仅是“能不能找到未读按钮”还要区分自己正在执行的动作属于哪一类。一个合格的脚本应该先告诉用户我默认要处理的是哪种消息使用哪种动作误伤范围是什么。如果直接把所有按钮都当成同一个处理对象那不是消息清理助手那是网页拆弹器。1.2 脚本适合谁不适合谁在花时间安装、测试甚至自己写脚本之前先判断场景合不合适。适合的情况大约是这是你自己本人的B站账号不是公司账号、家人账号、公共账号。消息记录本身不重要清理掉不会影响你后续找资料。你面对的是大量重复操作比如几百条广告私信一条条点太浪费时间。你用的是桌面浏览器或者脚本管理器能正常注入的移动端浏览器环境。你有能力在脚本出问题后看懂控制台日志至少能刷新页面恢复状态。不适合的情况也会很明确消息里可能有重要资料你还希望以后翻到。账号多人共用你不知道清理会不会影响另一个人。你基本不用浏览器网页端只在原生 App 里看消息。你要用的是一个黑盒脚本无法审查代码来源也不敢确认它到底访问了什么。你设备上的用户脚本管理器本身不可信或者来自非官方渠道。这里的核心判断是便宜好用的批量清理工具有一个共同前提就是操作对象足够低价值、重复度足够高、路径足够统一。一旦涉及“重要记录”或者“账号权限”批量自动化带来的风险就会超过它节省的时间。2. 从 v0.1 到 v0.2一个小工具迭代时到底在改什么2.1 页面是一张会变的网而自动化需要状态机如果只写一个“一次性脚本”思路通常很直接进入列表页找到所有消息逐个点击删除按钮。这种写法在 v0.1 里很常见也确实能跑通但跑通几次后你会遇到一些怪场景只处理了列表可视区域内的几条往下滚动后新加载的消息没被处理。因为点击过快页面还没刷新旧节点已经被移除脚本拿到的选择器失效。删除到一半某条消息的加载状态异常后续代码直接报错退出。处理完一批页面消息数已经变化但脚本中的计数还停留在最初状态。手动操作时人脑会自动等待页面完成加载、确认按钮状态变化、再决定要不要继续。但脚本没有这种本能它只会按照你写的顺序执行。如果没有“处理一批 - 确认结果 - 重新扫描 - 再处理下一批”的状态推进批量执行就会变成一连串不可控的点击。这正是 v0.2 和 v0.1 最容易拉开差距的地方。v0.1 证明的是能找到按钮能点击能完成一次手工替代。v0.2 要解决的则是能不能在多轮处理中仍然保持稳定能不能在异常发生时停下来。2.2 v0.2 常见迭代方向如果我没有拿到源码我不敢说这个项目的具体 v0.2 改动是什么。但从同类脚本的经验看从 v0.1 到 v0.2 往往不是增加花哨功能而是修补那些单次跑通时不会被发现、批量运行时会暴露的问题。v0.1 常见痛点v0.2 常见完善方向背后的本质原因在列表页连续执行后卡死增加每轮数量上限和间隔页面渲染跟不上脚本点击速度只在桌面端一个URL下有效增加移动端页面适配不同端的 DOM 结构并不是一套换一台电脑就不会用了减少外部依赖使用浏览器页面自带登录态用户脚本本身没有跨设备保存账号状态的能力误删会话后没法恢复默认只做“标记已读”不自动清理会话删除是不可逆的高风险动作出错后不知道卡在哪一步给每一步操作写日志和状态自动化必须可以被回看否则没法排查这个表格能解释一个现象很多脚本作者在第一版时认为难点是“定位元素”但第二版时才会意识到真正的难点是“在页面不断变化的前提下仍然稳定执行”。2.3 一个脚本应该维护的四个状态我在看或写这类工具时如果它是批量执行而不是单次点击会特别关注它有没有这四个状态未启动脚本已挂载但没有开始批量操作。运行中正在扫描与处理当前轮次和进度必须可见。暂停/停止用户能随时中断后续动作而不是等着几千条消息跑完。结束到达设定上限、没有更多目标或者发生了需要人工介入的错误。这四个状态如果能在日志里完整看到那这个脚本是具备基本工程意识的。如果没有你相当于在开一辆没有仪表盘和刹车的车速度快慢全凭运气。注意判断一个消息清理脚本能不能长期用不是看它的主页描述多漂亮而是看它出错时会不会停下来告诉你我现在停在哪一步最后执行了什么。3. “多端免登录”不是绕过登录而是把登录态和操作解耦3.1 最容易产生的误解看到“免登录”三个字容易有人兴奋是不是不用登录B站账号就能把消息清掉答案很直接不是。一个合规的用户脚本不应该也不需要绕过网站的登录逻辑。油猴脚本运行在你已经打开的网页环境里它之所以能操作你的消息是因为浏览器本身已经处在一个登录状态。脚本做不了“没登录就访问你的私人列表”这件事如果它真的能做到那它依赖的就不是公开接口而是某种你本不该触碰的漏洞。这里要把安全边界说清楚不管叫“免登录”还是“自动登录”都不应该变成“脚本帮你搞定账号状态”。你愿意把密码和手机验证码交给一个陌生脚本吗当然不应该。对于这类页面内工具最可靠的方式是你正常登录B站脚本只负责在你登录后的页面环境里做重复操作。3.2 常规实现路径从实现层面看一个规规矩矩的消息清理脚本通常走这几条路线之一策略“免登录”的实际效果需要注意的边界页面内按钮操作用户已经登录脚本模拟点击网页按钮操作范围不会超过当前页面能做的事调用页面自身请求链路请求携带浏览器已有的会话状态不需要额外输入账号只应处理当前账号数据不做改包、绕验证使用本地配置存储用户换设备后只需同步脚本偏好不暴露身份凭据脚本自身状态和登录状态分离这里所谓“免登录”更准确的表述应该是免掉脚本自己的登录步骤而不是免掉网站上那个正常登录步骤。用户不需要在油猴脚本里再输入一次账号或扫码打开已经登录好的页面就能直接用。这个体验优化很实际因为让你每台电脑都重新登录一遍B站已经够麻烦再让你在脚本里配一套独立凭证很多人会直接放弃。3.3 多端到底是什么端一个用户脚本要支持多端尤其是桌面端和移动端时并不是加一行 match 就行。桌面浏览器访问的页面结构、按钮位置、加载方式和手机浏览器访问的页面可能完全不同。即使网址看起来接近移动端页面可能更简化操作入口也可能不是直接显示而是藏在某个菜单里。如果你拿到一个号称支持多端的脚本建议在两种环境里分别验证而不是只检查安装页。更要紧的是如果手机端没有可靠的脚本管理器那就先别强行使用。油猴脚本依赖的注入机制并不是所有移动端浏览器都提供在没有可靠运行环境的情况下讨论多端免登录没有意义。4. 按这个最小流程写一个消息清理助手先扫清消息再谈批量4.1 先搭一个稳定、可见、可停止的脚本骨架不管是要写自己的脚本还是改造一个现成脚本来学习我建议从最基础的骨架开始不要一上来就写删除逻辑。在油猴脚本管理器里新建脚本时用户脚本头部是关键。一个适合做验证的骨架类似于// UserScript // name B站消息清理助手示例 // namespace bilibili-message-cleaner-demo // version 0.2.0 // description 一个用于演示B站消息批量管理思路的脚本骨架 // match https://www.bilibili.com/* // match https://m.bilibili.com/* // grant none // run-at document-idle // /UserScript (() { use strict; const config { autoMarkRead: true, autoDeleteSession: false, maxPerRound: 20, delayMs: 400, stopOnError: true, }; function log(action, result) { console.log([消息清理助手] ${new Date().toLocaleTimeString()} | ${action}, result); } log(脚本已挂载当前页面, location.href); })();这段代码不算成品只是脚本骨架。从这里可以看出脚本默认并不打算执行任何删除也把autoDeleteSession默认关掉了。先让脚本只输出日志确认它能稳定注入到目标页面再继续往下写是可靠的顺序。4.2 先做扫描不要一上来就执行真正动手写批量逻辑前先在浏览器控制台做一次人工验证。打开消息列表找到网页上某条消息对应的元素确认它的可识别属性。不同版本页面上结构并不一致所以不存在一成不变的选择器。一个示例性的扫描逻辑是// 示例先只统计不做任何删除操作 function collectItems(root) { return Array.from(root.querySelectorAll([data-message-item])); } const items collectItems(document); log(当前列表里能拿到的消息项数量, items.length);如果页面不存在>function sleep(ms) { return new Promise((resolve) setTimeout(resolve, ms)); } async function runRound(round) { if (round config.maxPerRound) { log(到达本轮处理上限停止); return; } const items collectItems(document); if (!items.length) { log(当前没有可处理项); return; } const target items[0]; // 这里只处理“标记已读”这类低风险动作 if (config.autoMarkRead) { target.querySelector(.read-btn)?.click(); } await sleep(config.delayMs); if (config.stopOnError) { // 在真实实现里这里应该检查上一轮请求是否成功 // 如果失败直接停下不要继续处理下一条 } runRound(round 1); }这个思路的关键不是代码技巧而是它承认了页面是活的、请求需要时间、异常需要中断。处理完一条不急着往下一条冲而是重新确认一遍当前列表状态再决定下一步。4.4 不可逆操作必须默认关闭我建议任何消息清理脚本把两类动作分开低风险动作标记已读、展开通知、忽略消息。这些动作不影响会话记录本身。高风险动作删除会话、清空消息记录。这些动作不可恢复。在这类脚本里高风险动作应该是一个显式开启的配置而不是默认执行项。把“删除会话”放在默认行为里等于把一个只能在特定场景使用的武器绑在腿上。一旦选择器误匹配几条重要会话就会瞬间消失。好的脚本应该像这样区分if (!config.autoDeleteSession) { // 当前脚本不允许删除会话 // 那么这里会跳过所有包含“删除”关键词的按钮 return; }如果拿到的一个脚本默认就把删除按钮一起点了也没有二次确认和停止按钮我建议不要在生产环境用。它或许能跑得快但快不意味着安全。常见经验是先在小号上制造少量测试消息用单轮方式跑通一次观察只处理了目标消息再逐步加大轮次确认无误后才轮到真正账号里的历史消息。任何跳过这一步的人都是在拿自己的历史记录做测试。5. 多端上线前必须想清楚的四件事5.1 URL 命名空间没有你想象的那么宽一个脚本如果同时匹配桌面网页和移动端网页需要在脚本头部认真设计 match 范围。全站匹配看似方便实际会让你在浏览普通视频页、稍后再看、收藏夹等无关页面时也触发脚本逻辑干扰正常浏览。比较合理的做法是尽量让脚本只在消息中心或目标列表相关的 URL 范围内运行。具体包含哪些子目录需要打开 DevTools 看实际页面地址。不同阶段的页面可能调整过路由所以“上一个教程里的匹配规则”不一定永远有效。5.2 功能开关必须独立且默认保守在多端场景里最容易出现的问题是桌面端配置了“自动删除会话”移动端却默认关闭结果用户在两台设备上得到的体验完全不一致。这种问题不一定是 bug而是缺少“功能开关同步”的意识。脚本可以把配置分成两类安全设置如是否允许删除会话、是否从外部更新代码、是否启用日志。使用偏好如每轮处理数量、间隔时间、是否自动滚动。安全设置在每台设备上都应该默认采用最保守值不能为了体验一致就强制同步。你要的是多端都能用不是多端一起去执行一个高风险配置。5.3 权限边界必须保持最小油猴脚本头部里的 grant 字段决定了脚本能调用哪些能力。一个只用页面 DOM 的脚本使用grant none就够了。如果脚本申请了跨域请求、访问剪贴板、下载文件等能力你需要想清楚它为什么要这些东西尤其在安装别人写的脚本时我会先看两点是否引用了远程地址在运行时动态拉取新代码。是否存在混淆严重的代码无法判断它到底发送了什么请求。用户脚本不是越底层越厉害反而是越容易审计越值得信任。一个开源、可读、只做一件事的脚本好过一个功能丰富但读不懂的脚本。这不是保守是必要的风险管理。5.4 上线前先在小号上做灰度脚本开发里也应该有“灰度”意识。第一次安装只观察不执行。 第二次执行只处理一条消息。 第三次执行处理少量消息并检查日志。 第四次执行才把轮次上限提高。每一步都要确认这次操作有没有产生预期中的请求有没有把不该删的会话删掉有没有出现未读数已经清零但页面仍然显示旧状态的情况6. 消息没清掉、误清了、脚本不跑了按这个顺序排查6.1 先看现象是哪一层的问题脚本不生效时不要急着改代码。第一步先确定问题出在脚本层、页面层还是网络请求层。打开浏览器控制台看脚本有没有输出日志。如果脚本完全没运行检查脚本管理器是否开启、match 是否覆盖当前 URL、页面有没有正常登录。如果脚本运行了但没有找到目标元素先手动在 DevTools 里执行选择器确认元素路径是否存在。如果按钮点击了但消息没变看 Network 面板里请求是否发出有没有返回错误状态码。一个很常见的问题是页面上按钮被点击后只是把未读数字从 UI 上隐藏后台请求并没有成功。UI 成功不等于业务成功这也是脚本需要记录网络状态和响应结果的原因。6.2 执行到一半停下来绝大多数是列表或请求状态异常脚本处理几十条后停止可能是出现了以下几种情况之一上一轮点击已经让 DOM 节点变化下一轮还引用旧节点导致代码报错。网络请求被限流返回了异常状态。页面的虚拟列表还没有滚动到下一区域脚本找不到可处理的消息。某条消息的界面加载失败按钮位置和预期不同。这时应该看脚本最后一条日志和 Network 面板里最近一次请求的状态。不要直接改大间隔、改小间隔先找到停住的真正原因。6.3 节流参数不是越大越好很多初学者认为处理消息时延时要越长越安全于是设置成 3 秒甚至 5 秒一条。这确实安全但几百条消息就会等得失去耐心。真正合理的做法是先从 300 到 800 毫秒之间开始测试观察请求是否稳定再找到一个既不触发页面异常、又能接受的速度。比单条延迟更关键的是“轮次上限”和“失败中断”。将每轮处理数量限制在一个合理范围比如 20 到 50 条比无限循环更可控。如果第一轮出现问题你还可以在几十条内停住而不是等几百条删完才发现错误。6.4 多端排查的顺序不太一样桌面端脚本不跑了检查顺序通常是脚本开关 - match - 登录态 - 元素选择器。移动端脚本不跑了还要多检查一步脚本管理器在移动端浏览器里到底有没有真正注入。如果它只是把脚本文件装上了但页面没有加载那么问题不在代码而在运行环境。排查时可以直接在目标页面开一个脚本内部的调试模式把页面地址、当前元素数量和最后执行状态输出到 localStorage。这样即使你关掉页面也能在下一轮启动时读到最后一次日志知道问题出在哪一步。| 排查对象 | 第一眼要看什么 | 需要避免的误区 | |