B站 @ 回复流程
让 AI 读懂你在 B 站 @ 它的视频,然后在「@我的」界面里回你一条带可跳转时间戳的评论。
依赖(缺任何一样都跑不起来)
| 依赖 | 说明 |
|---|---|
| 浏览器自动化 ⚠️ | 最关键。AI 必须能操控一个已登录 B 站的浏览器,否则读不到 @ 通知、也发不了评论。推荐 BrowserSkill(bsk CLI + 浏览器扩展)。安装步骤见仓库 README 的「前置条件」一节。 |
video-watch 技能 | 必需。用它把视频转成带时间戳的文字稿 + 抽帧图片。本技能不重复实现这一步。 |
| 一个 B 站账号 | 建议用不常用的小号;本技能会操作它的评论区。 |
本技能的示例按 DSH 的 browser_* 工具写。 换环境时把这一步换成你手上的浏览器工具,流程逻辑不变——读通知、核对昵称、点回复框这几步是一样的。
装完先自检:能在浏览器里打开 https://message.bilibili.com/#/at 并读到通知列表,才算前置通了。
首次配置
- 把
config.example.md复制成config.md - 按里面说明填入你的昵称和机器人账号昵称 / UID
- 确认
video-watch技能已装好(见它的 README) - 确认浏览器自动化可用(见上「依赖」一节,这是最容易漏的一步)
为什么要填两个昵称:本技能的设计前提是「你用账号 A @ 自己」——也就是发 @ 的人和执行回复的账号是同一个。所以不能靠 UID 区分,只能靠昵称核对。
第一步:核对身份(最重要,别跳过)
@ 列表里混着别人的通知。只回目标用户发的,别人的绝对不能回。
回复前必须走完这个清单(配置见 config.md):
① 打开 https://message.bilibili.com/#/at ② 找【最新一条】通知 —— 不要盲取列表第一条,要看时间戳 ③ 核对:这条通知显示的【发 @ 人昵称】== config.md 里的 owner_name ? ④ 点这条通知的【评论文本】→ 跳转到视频(URL 带 #reply<评论id>) ⑤ 在视频评论区确认那条评论的作者昵称 == owner_name ⑥ 确认评论里 @ 的账号 == config.md 里的 self_name ⑦ 全部对上 → 才能回复
任何一项对不上 → 停下来问用户,不要擅自回复。
为什么要有这一步(真实事故)
最初的版本写的是「取 @ 列表第一条」,没有身份核对。结果 @ 列表里混着第三方一次 @ 一串人的通知,于是点错了别人的视频、还发了评论。
教训:@ 通知只显示评论文字,不显示是哪个视频,所以「第一条」这个判断根本不可靠。
第二步:分析视频
先拿免费素材:简介 / 官方字幕 / 弹幕摘要
别一上来就抽帧。 B站 的视频简介里经常自带章节时间轴、BGM 名、拍摄地;很多视频本身带校对过的官方字幕;弹幕密度能告诉你观众在哪里有反应。
tools/bili-context.ps1 一次拿齐这三样(走浏览器登录态,零额外成本):
powershell. "<repo>/tools/bili-context.ps1" $runner = { param($a) Invoke-BskTab $sid $tab $a }.GetNewClosure() $ctx = Get-BiliContext -Runner $runner -Aid <aid> # 或 -Bvid BVxxxx # $ctx.desc / $ctx.subtitle / $ctx.danmaku_digest / $ctx.danmaku_count
- 有字幕就用字幕,并给下面的
watch.py加--no-transcribe跳过本地转写(字幕比 whisper 准) - 弹幕摘要约 200 字(每分钟密度 + 高能处最多 8 条原话)。它只代表观众反应,不代表画面事实 —— 空镜类视频弹幕稀疏,别把「响」当「好看」
再跑 video-watch 拿转写与画面
拿到 BV 号后,用 video-watch 技能处理(先加载该技能):
bash# 按你的实际路径调整 python <video-watch>/scripts/watch.py "https://www.bilibili.com/video/BVxxxx/" --language zh --model small --max-frames 10
读 <run_dir>/transcript.txt(每行 [MM:SS] 文本)。
⛔ 转写空了就用 --no-vad 重跑
歌曲 / 翻唱 / 纯音乐视频,VAD(人声检测)会把歌声当噪声滤掉,结果是 0 段:
transcript.txt0 字节manifest.json里transcript.segments= 0
这时不要放弃、更不要去硬读几十张图(那是烧 token 的真正原因)。直接重跑:
bashpython <video-watch>/scripts/transcribe.py \ --video "<run目录>/<视频文件名>" --out-dir "<run目录>" \ --model small --language zh --no-vad
实测:4 分 12 秒歌曲,vad_filter=True → 0 段;加 --no-vad → 57 段。
预防:判断是歌曲/纯音乐,一开始就加 --no-vad。
抽帧图片:只读你真正需要的那几张
- 主依据永远是文字稿。 一张图 ≈ 1500 字,读 80 张 ≈ 12 万字。
- 一次最多读 3~5 张,挑画面变化大的时刻。
- 音频类视频(歌/播客/访谈)不要读图 —— 文字稿已经够了。
- 文件名必须从
frames/frames.json的file字段读,不要凭时间戳猜。 (曾经猜0079_t000249.5.jpg,实际是0079_t000249.6.jpg,读图直接失败。)
第三步:回到「@我的」发送
不要去视频评论区的顶部输入框发 —— 那会变成独立顶层评论,对方收不到「有人回复了你」的通知,会以为你没回。
正确做法:
回到 https://message.bilibili.com/#/at → 点那条通知上的「回复」按钮 → 通知下方展开内联回复框 → 填入内容 → 点「发表评论」
成功后页面会弹「评论成功」气泡。
回复形式:按视频类型决定
先判断视频属于哪一类,再决定怎么写。不是所有视频都要带时间戳。
教学 / 讲解 / 知识类 → 详细分析 + 时间戳
看点是「信息」,读者希望拿到能复用的干货:
- 用文字稿分段,每段配可跳转时间戳(
MM:SS,超过 1 小时用HH:MM:SS) - 时间戳必须是纯文本
00:00开头,B 站才会自动转成跳转链接 - 结构:一句总评 → 章节时间戳列表 → 一句补充
玩乐 / 搞笑 / 音乐 / 二创 / 日常类 → 分享情绪,不用时间戳
看点是「感觉」,堆时间戳会很出戏:
- 不要时间戳,不要章节列表
- 一到三句情绪反应:接梗、吐槽、共情
- 可以短到一句话
基调偏暗 / 情绪沉重的内容 → 先接住情绪
如果视频涉及悲伤、告别、自伤等沉重主题:
- 不要用轻快或开玩笑的语气
- 先接住情绪,人设可以保留但要收着
- 拿不准时,宁可简短、克制,也不要硬搞笑
拿不准时
偏中间的内容(测评、杂谈、vlog)默认走情绪路线。
语气
默认用中性、友好、像真人随手回一句的语气。
如果 config.md 里配了自定义人设,按那个写 —— 但注意:人设只作用于发出去的评论内容,跟用户的对话和工具操作保持正常工作语气。
可选:让它自己盯着(自动检查 + 自动回复)
不想每次都喊它?tools/watch-at.ps1 把整套流程变成后台自动化:每 30 秒检查一次 @,
命中就自己看视频、自己回评论,再用微信告诉你回了什么。检查那一步不花模型 token ——
脚本直接在已登录的浏览器里调 B 站接口,只有真要回复时才叫一次模型。
powershellcd <技能目录>\tools Copy-Item watch-at.config.example.ps1 watch-at.config.ps1 # 填路径与两个 UID .\watch-at.ps1 -Init # 首次接入:把当前最新一条设为基准,历史积压不处理 .\watch-at.ps1 -Test # 给微信发一条测试消息 .\watch-at.ps1 -InstallTask # 装计划任务(1 分钟一轮 + 脚本内 30 秒补检)
白名单:只回 $OwnerMid 那个人的 @,而且被 @ 的必须是 $SelfMid ——
群里那种一次 @ 一串人的通知会被自动丢掉,这正是最容易出错的地方。
节拍、阈值、以及这套东西踩过的坑,见 tools/README.md。
已知的坑(都会真的发生)
| 坑 | 症状 | 处理 |
|---|---|---|
| 点用户名 | 跳到对方主页 | 要点的是评论文本 |
| 用顶部输入框回复 | 变成独立评论,对方收不到通知 | 用通知里的「回复」按钮 |
| 消息中心白屏 | 页面空白 | reload 一次通常就好 |
| 点进「回复我的」 | 那是别人回你的,不是 @ | 只处理「@ 我的」 |
| B 站风控 | 弹验证码 / 接口返 -352 | 别连发太多条,隔开时间 |
| 浏览器 daemon 空闲超时 | 报 session not registered | 重启 daemon 后重建会话(约 10 秒) |
| 转写 0 段 | 歌曲类视频 | 加 --no-vad 重跑 |
| 评论发出去了、别人看不到 | 接口回 code 0,自己看得见、别人看不见(影子隐藏) | 发送前先切到那条视频页;发完用匿名请求复查可见性 |
| 计划任务里中文全乱 | PowerShell 没有控制台时用 GBK 解 UTF-8,JSON 直接解析失败 | 原生命令输出重定向到文件、再按 UTF-8 读 |
权限与边界
- 本技能会以你的身份在公开评论区发言。请自行确认发出去的内容得体。
- 建议用小号;自动化操作有触发平台风控的可能。
- 不要在一条视频下连发多条,也不要批量刷。
自动化细节(v2)
无窗口运行
计划任务不要直接跑 powershell.exe —— 那样每轮会闪一个控制台窗口。用仓库里的隐藏启动器:
任务动作:wscript.exe //B //Nologo "<repo>\tools\run-hidden.vbs" 运行级别:最低权限(/RL LIMITED)
run-hidden.vbs 用 WScript.Shell.Run ...,0,False 启动,脚本内的「30 秒补检」也走同一个 vbs。
30 秒节拍怎么来的
计划任务最快 1 分钟一轮,所以脚本每轮收工时用 -SecondPass 先睡 30 秒再跑,把节拍补成约 30 秒。别改成立刻拉起 —— 那等于连着跑两遍,不是 30 秒。
浏览器没开就自动拉起
每轮先查 bsk browsers;没连上就检查 Get-Process chrome,没开就启动 Chrome(最小化打开 bilibili.com),然后 3 秒一查、最多等 45 秒等扩展连回来。
并发闸
check.lock:22 秒内已有轮次在跑就直接让路,防止两轮同时读到同一条 @ 而重复回复。
失败的 @ 会被吞
只要进了回复分支,无论成败都会推进 at-state.json 的 cursor 并写入 seen —— 这条 @ 不会再重试。排查失败原因后要手动把 cursor 退回去、把 seen 清空。
关键帧怎么挑
不是均匀抽样,是对齐台词时间点:从字幕/转写抓所有 [MM:SS] → 每个时间点取最近的帧(差 ≤6 秒)→ 时间差 <1.2 秒的算同一处去重 → 不够预算再均匀补齐。
预算:全片 ≤60 秒读 8 张,≤180 秒 6 张,更长 5 张,纯音乐 3 张。
抽帧参数
--max-frames 10,模式用默认 auto(场景点 + 均匀补齐)。纯 --mode scene 在短视频上会饿死 —— 实测 12 秒视频只出 2 帧。
更多踩过的坑
| 坑 | 症状 | 解决 |
|---|---|---|
.ps1 没有 BOM | PowerShell 5.1 按 ANSI 读中文 → ParseError / 中文全乱 | 存成 UTF-8 with BOM |
空数组被展开成 $null | 接口回 0 条被误判成「取数失败」,反复重试后跳过 | return ,@($payload.items) |
av 版本不兼容 | open() got an unexpected keyword argument 'metadata_errors' | 删掉 faster_whisper/audio.py 里那个参数(升级会被还原) |
| headless 缺 API key | 两秒就报「写评论失败或跑偏」 | dsh --profile headless 不读 credentials 的 refs,要先把它放进环境变量 |
| 没字幕就以为没素材 | 音乐/空镜类完全读不出内容 | 至少拿简介 + 弹幕摘要 |


