🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

B站视频解读 Sacredbluedragon/dsh-bili-watch

DeepSeek Harnessv0.3.8
Sacredbluedragon1 天前有提交已提供中文文档扫描:含敏感能力

给模型看 B 站视频的 DSH 插件:字幕、转写、抽帧 OCR、评论与弹幕;支持 b23.tv 短链,抽帧缓存目录可换。

36.7/100
综合分
6
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/31 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-bili-watch
npm 源 · dsh-bili-watch 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · platform
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 1 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-bili-watch @ 0.3.8
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖要求 >=0.2.0-rc.2 <0.3.0-0 · 最新 0.2.1-rc.5 兼容
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/30 21:12:11

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 358 处,展示前 60 条)
  • 写入本地文件
    index.js:60
    mkdirSync(CACHE, { recursive: true })
  • 读取本地文件
    index.js:63
    for (const d of readdirSync(CACHE)) {
  • 写入本地文件
    index.js:68
    mkdirSync(d, { recursive: true })
  • 写入本地文件
    index.js:102
    writeFileSync(f, s, 'utf8')
  • 读取本地文件
    index.js:163
    const st = statSync(src)
  • 写入本地文件
    index.js:165
    mkdirSync(dst, { recursive: true })
  • 读取本地文件
    index.js:166
    for (const e of readdirSync(src, { withFileTypes: true })) copyTreeSync(join(src, e.name), join(dst, e.name))
  • 写入本地文件
    index.js:168
    copyFileSync(src, dst)
  • 读取本地文件
    index.js:181
    if (existsSync(DEPS_MOVED_MARK)) return DEPS_PREF
  • 读取本地文件
    index.js:183
    if (existsSync(DEPS_PREF)) return DEPS_PREF
  • 读取本地文件
    index.js:185
    try { srcHas = existsSync(DEPS_LOCAL) && readdirSync(DEPS_LOCAL).length > 0 } catch {}
  • 写入本地文件
    index.js:186
    if (!srcHas) { try { mkdirSync(DEPS_PREF, { recursive: true }) } catch {} ; return DEPS_PREF }
  • 删除本地文件递归
    index.js:201
    try { rmSync(tmp, { recursive: true, force: true }) } catch {}
  • 删除本地文件递归
    index.js:203
    try { rmSync(DEPS_PREF, { recursive: true, force: true }) } catch {}
  • 删除本地文件递归
    index.js:208
    try { rmSync(tmp, { recursive: true, force: true }) } catch {}
  • 删除本地文件递归
    index.js:537
    try { rmSync(p, { recursive: true, force: true }); files += st.files; bytes += st.bytes } catch (err) { failed.push({ name: e.name, why: String(err?.m
  • 删除本地文件
    index.js:539
    try { const st = lstatSync(p); rmSync(p, { force: true }); files += 1; bytes += st.size } catch (err) { failed.push({ name: e.name, why: String(err?.m
  • 删除本地文件
    index.js:544
    try { const st = lstatSync(p); rmSync(p, { force: true }); files += 1; bytes += st.size } catch (err) { failed.push({ name: e.name, why: String(err?.m
  • 执行外部命令来自配置/变量
    index.js:707
    execFileSync(c, ['-version'], { stdio: 'ignore', timeout: 1200, windowsHide: true })
  • 执行外部命令固定命令
    index.js:839
    execFileSync('taskkill', ['/PID', String(c.pid), '/T', '/F'], { windowsHide: true })
  • 执行外部命令来自配置/变量
    index.js:864
    child = spawn(process.execPath, [LOOK, ...args], {
  • 发起网络请求
    index.js:928
    const r = await fetch(SPI, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(8000) })
  • 发起网络请求
    index.js:940
    const r = await fetch(QR_GEN, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(8000) })
  • 发起网络请求
    index.js:960
    const res = await fetch(QR_POLL(key), { headers: { 'User-Agent': UA }, redirect: 'manual', signal: AbortSignal.timeout(8000) })
  • 执行外部命令固定命令
    index.js:1162
    const ps = spawn('powershell.exe', ['-NoProfile', '-STA', '-Command', script], { windowsHide: false })
  • 执行外部命令来自配置/变量
    index.js:1295
    ver = String(execFileSync(ytdlp, ['--version'], { encoding: 'utf8', timeout: 5000, windowsHide: true }) || '').trim().split(/ +/)[0]
  • 执行外部命令来自配置/变量
    index.js:1358
    execFile(file, args, opts, (err, stdout) => resolveP({ ok: !err, stdout: String(stdout ?? ''), err }))
  • 发起网络请求
    client.js:89
    r = await fetch(API + p, { credentials: 'same-origin', signal: guard.signal, ...init })
  • 读取本地文件
    tools/look.mjs:51
    if (existsSync(pref)) return pref
  • 读取本地文件
    tools/look.mjs:52
    return existsSync(local) ? local : pref
  • 读取本地文件
    tools/look.mjs:62
    raw = readFileSync(p, 'utf8')
  • 写入本地文件
    tools/look.mjs:74
    try { renameSync(p, bak); note = `, backed up as ${basename(bak)}` } catch { note = ', backup failed' }
  • 读取本地文件
    tools/look.mjs:81
    const pick = (...cands) => cands.find((p) => existsSync(p)) ?? cands[cands.length - 1]
  • 读取本地文件
    tools/look.mjs:102
    for (const e of readdirSync(dir, { withFileTypes: true })) {
  • 读取本地文件
    tools/look.mjs:112
    else { try { bytes += lstatSync(p).size } catch {}; files += 1 }
  • 删除本地文件递归
    tools/look.mjs:191
    try { rmSync(p, { recursive: true, force: true }); files += st.files; bytes += st.bytes } catch (err) { failed.push({ name: e.name, why: String(err?.m
  • 删除本地文件
    tools/look.mjs:193
    try { const st = lstatSync(p); rmSync(p, { force: true }); files += 1; bytes += st.size } catch (err) { failed.push({ name: e.name, why: String(err?.m
  • 删除本地文件
    tools/look.mjs:198
    try { const st = lstatSync(p); rmSync(p, { force: true }); files += 1; bytes += st.size } catch (err) { failed.push({ name: e.name, why: String(err?.m
  • 写入本地文件
    tools/look.mjs:246
    mkdirSync(want, { recursive: true })
  • 写入本地文件
    tools/look.mjs:249
    writeFileSync(mark, JSON.stringify({ version: 1, createdAt: new Date().toISOString() }), 'utf8')
  • 写入本地文件
    tools/look.mjs:255
    writeFileSync(probe, 'ok')
  • 删除本地文件
    tools/look.mjs:256
    rmSync(probe, { force: true })
  • 写入本地文件
    tools/look.mjs:262
    try { mkdirSync(defaultDir, { recursive: true }) } catch {}
  • 写入本地文件
    tools/look.mjs:267
    if (!existsSync(m)) writeFileSync(m, JSON.stringify({ version: 1, createdAt: new Date().toISOString(), fallback: true }), 'utf8')
  • 发起网络请求
    tools/look.mjs:457
    r = await fetch(url, { headers, signal: AbortSignal.timeout(timeoutMs) })
  • 发起网络请求
    tools/look.mjs:798
    r = await fetch(url, { headers: { ...H, Referer: REF }, signal: AbortSignal.timeout(120000) })
  • 删除本地文件
    tools/look.mjs:899
    unlinkSync(f)
  • 删除本地文件
    tools/look.mjs:912
    unlinkSync(f)
  • 执行外部命令来自配置/变量
    tools/look.mjs:975
    execFileSync(c, ['-version'], { stdio: 'ignore', timeout: 300000 })
  • 执行外部命令来自配置/变量
    tools/look.mjs:1002
    return execFileSync(bin, args, opts)
  • 执行外部命令来自配置/变量
    tools/look.mjs:1696
    const r2 = spawnSync(ASR_PY, [ASR_SCRIPT, wav, '-o', cacheTxtTmp, '--model', model, '--device', tuning().asrDevice, '--json'], {
  • 执行外部命令来自配置/变量
    tools/look.mjs:1790
    const out = execFileSync(OCR_PY, args, {
  • 请求外部主机comment.bilibili.com
    tools/look.mjs:2163
    const r = await fetch(`https://comment.bilibili.com/${cid}.xml`, {
  • 发起网络请求
    tools/look.mjs:2163
    const r = await fetch(`https://comment.bilibili.com/${cid}.xml`, {
  • 发起网络请求
    tools/look.mjs:2643
    const r = await fetch(SPI, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(15000) })
  • 发起网络请求
    tools/look.mjs:2652
    const res = await fetch(qrPoll(key), { headers: { 'User-Agent': UA }, redirect: 'manual', signal: AbortSignal.timeout(15000) })
  • 发起网络请求
    tools/look.mjs:2737
    const r = await fetch(QR_GEN, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(15000) })
  • 执行外部命令来自配置/变量
    tools/look.mjs:3286
    const out = execFileSync(ffprobe, ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', file], {
  • 请求外部主机api.bilibili.com
    tools/look.mjs:3641
    const r = await fetch(`https://api.bilibili.com/x/web-interface/view?aid=${encodeURIComponent(aid)}`, {
  • 执行外部命令来自配置/变量
    tools/look.mjs:3740
    execFileSync(c, ['--version'], { stdio: 'ignore', timeout: 300000 })
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
涉及外部主机:comment.bilibili.com、api.bilibili.com

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/dsh@deepseek-ai/dsh-tools@deepseek-ai/dsh-llm
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
v0.3.92026-10-31
· 新增 · text / sub 输出带逐句时间码:纯文本每行前面多了 [mm:ss],可以直接定位到某一刻;不想要时间码加 --no-time,退回和以前一样的纯文本。 · peek 不再每次下视频:先取 B 站官方截帧接口,拿到就直接用;拿不到才回落下载 360p 抽帧。多数情况下同样是 5 帧,省掉一次下载。 · frames --at 精看某一刻或某一段:--at 750(秒)、--at 12:30(分:秒)、--at 12:30-13:10(区间,默认每 5 秒一张)。 · 抽帧索引格式统一:现在是 r1c1 00:00-00:05 @00:02 这种写法,图上的编号和索引里的编号是同一套,不用再自己数。 · 修复 · --help 会真的执行那个命令。之前 clean --help 想看一眼用法,结果直接把缓存清了。现在带 --help 只打印帮助,任何命令都不执行。 · frames --at 换一个时刻,仍然返回上一次的帧。之前不同时刻会落到同一份缓存,退出码 0、输出看着正常、给的是错的那批图。 · peek 拼版图上的时间码是估算的(按百分比推算),和帧的实际时刻对不上,越靠后偏得越多;另外最后一帧会取到拼版图的黑色空格。现在用接口给的真实时刻,格号也对齐了。 · peek 遇到非方形的拼版图(不是 10×10)会把行列算错,取到相邻格的图。 · GPU 探测失败时只说「运行库未装」,实际可能是探测超时;判错之后还会把这个结论缓存 5 分钟,装好了也不会自己恢复。现在会报具体的失败原因,不确定的结论不再写进缓存。 · 文档 · --at 只对 frames 生效(之前写成了 frames / sheet,sheet 实际不认这个参数)。 · --head 与 --out 按实际行为改写:不传 --head 是按 800 字 / 400 字的规则自动截断,不是输出全文(要全文用 --full);--out 只在部分命令上有效,sheet / ocr / substrip 固定写缓存目录。 · 补一句缓存目录的归属:clean 和面板的「立即清除」是按名字整棵删 _logs frames sheets sheet-tmp substrip-tmp _misc asr frames-dl audio-dl _hot 这 10 个子目录,里面放什么都不会保留。别把自己的文件放进这些名字的目录。放在缓存根根目录下、名字不在上表里的文件不会被删。 · 另外改正了几处与代码对不上的说法:deps/ 在 $DSH_HOME/bili-watch/deps 而不是跟着卸载删、运行时参数是 18 项不是 17 项、BILI_ASR_DEVICE 环境变量不生效(要改配置里的 asrDevice)。
v0.3.82026-10-30
· 凭证只认自己那份 删掉了「跟随同机其它工具」整套逻辑:原来 watch 会去读同机某个业务工具的凭证文件, 谁的文件更新就用谁的,还会把其中一个小号复制回来。对没装那个工具的人是死分支, 对装了的人则是一个隐形的共享登录态——面板里删了凭证,下次仍然有登录。 现在只认 $DSH_HOME/bili-watch/credential.json(旧位置的凭证会一次性迁移过来)。 面板与 CLI 两侧同一套判据。 · 修掉的实质问题 · ASR 一跑就崩:asr 命令没有内存闸门,机器剩 0.8 GB 时直接进 faster-whisper, 撞 mkl_malloc 挂掉,崩之前还承诺「3 分钟片约 30–45 秒」。现在先算时长再决定跑不跑, 内存不够时给替代方案(调 --asr-max 或关掉转写)。 · --out 能写进系统目录:--out C:/Windows/Temp/x.json 会被自己的错误处理器绕开黑名单, 拒绝原因照样写进那个文件。现在被拒就退回 stdout,目标位置一个字节都不落。 · 整条链接当 BV 用:CLI 传 时,缓存目录会建成 httpswwwwbilibilicomvideoBV...,连自己都认不出来。现在两处都先抽 BV。 · clean 清不掉自己的垃圾:login-qr.png 和「建了但没写 meta 的空 BV 目录」永远留在缓存里, 每次都进「不是 bili-watch 的缓存条目,不动」。 · 删凭证只说成功:面板删了凭证,实际上业务侧那份还在被用,界面只报「已删除」。 · 其它 · 批量下载失败时能看到真实原因(原来只显示「后端未受理」)。 · /status 的缓存占用加了 30 秒节流,8 秒轮询不再反复全盘 walk。 · 坏掉的配置文件先留档 .broken- 再回默认值,不再静默擦掉用户设置。 · 自动清理与手点清理用同一套「这是我的目录」判据:只认带 meta.json 的 BV 目录, 你在缓存根里自己建的 BV... 目录不会被误删。 --- 感谢 @ppppiii 在 #4 里找到 Windows 一键下载从来没真正执行过 (spawn 的 detached: true 会让 powershell 起来就退出、脚本一行不跑,日志还写「成功」)—— 这是这一版所有「报成功但没做」类修复的起点。
v0.3.72026-10-30
· 这一版主要是把外部贡献合进来 感谢 @ppppiii 的 PR #4——他复现并修掉了一个从 0.2.x 就存在、我们所有代码审查都没发现的根因。 P0:Windows「一键下载」从来没真正执行过 · index.js 的 runDownload() 用 spawn(..., { detached: true }) 启 powershell。Windows 上 libuv 把 detached 映射成 DETACHED_PROCESS,powershell 起来约 0.5 秒就以 exit code 0 退出,-File 指定的脚本一行都不执行,stdout/stderr 全空 · 更糟的是 0.3.5/0.3.6 把子进程 stdio 重定向进日志并监听 exit,于是日志会写「结束,退出码 0(成功)」——看起来成功了,其实什么都没跑 · 修法:Windows 上不再 detached(Windows 本来就不会因父进程退出杀子进程);子进程输出改写到 deps\download.out.log,deps\download.log 只留宿主侧结构化事件(开始 / pid / detached / shell / 退出码) P0:开着系统代理的机器,脚本能跑了也会失败 · curl.exe / pip / huggingface_hub 都不读 Windows「Internet 选项」里的系统代理,报 curl: (52) Empty reply from server · 修法:新增代理探测链 -ProxyUrl > BILI_WATCH_PROXY > HTTPS_PROXY / HTTP_PROXY > 注册表 WinINet,并给 pip / huggingface_hub / uv 设好对应环境变量 Python 选择链重做 · 不再尝试 3.14:onnxruntime 目前没有 cp314 wheel,装了也跑不了 OCR · 顺序改为 -PythonPath > uv 3.12/3.13 > py 3.13/3.12/3.11/3.10 > PATH · uv venv 默认不装 pip(venv 里 python -m pip 不可用),失败时自动回退系统 Python 发布脚本的版本闸门(本版自查发现) · package.json 不在 files 数组里(npm 自动打包它),所以发布闸的 md5 比对永远比不到它——只改 dev 那份版本号不会触发同步,发布树仍是旧版,npm publish 报 cannot publish over the previously published versions · 修法:单独比 version 字段(不能进 md5 清单,sync 会用 ConvertTo-Json 重排格式),并给 sync-release.ps1 显式传 -Version · 验证 · 作者侧:控制变量(detached true/false)、PS5.1 与 pwsh7 两种 shell、真跑 ytdlp/ocr/asr 三个目标、失败路径(curl 52)、rebase 到 0.3.6 后重跑全过 · 本机侧:隔离目录 -Target ytdlp -Force 真跑,download.log 写出完整链路(脚本确实执行了);-Mirror cn 下载成功(yt-dlp.exe 23,575,744 字节) · 已知未修 · -Mirror auto 只探 huggingface.co 的 443,不探 github.com:huggingface 通、github 不通的机器会被判成直连而超时 · 0.3.6 留的六条小项(isOwnBvDir 零覆盖、面板报 skipped、USAGE_ONLY 漏参数错误、两处 dirStat 链接口径、探针路径假阴性、改缓存位置后旧提示最多晚 10 分钟消失)
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-bili-watch DSH 插件 / 命令行工具:把 B 站视频解析成模型能直接消费的结构化内容(元数据、评论、弹幕、AI 字幕、本地 ASR 转写、抽帧 OCR)。 为什么用它 - 本地跑不上传:ASR 转写 + OCR 抽帧全在本机,不依赖云 API、不花钱 - 防字幕诱饵:语速异常或字幕自称「实验/钓鱼」即判可疑;关键词命中率只在语速也异常时才作补充;可疑时加 --verify 抽 5 帧 OCR 比对画面(不加只提示一句) - 二次读瞬时:缓存目录 + 文件锁,同一视频二次读取秒出 - 为 AI 设计:manifest 落盘 + 摘要输出,智能体直接消费不用再解析 特性 - 支持 b23.tv 短链、BV 号、av 号及通用 URL(经 yt-dlp) - 降级链:AI 字幕 → 本地 ASR → 抽帧 OCR → 读图,四级全 miss 时退出码为 1 - 匿名可用:元数据、评论、弹幕、搜索、热榜无需登录 - 可选依赖:ffmpeg(抽帧)、Python venv(ASR/OCR)、faster-whisper(转写模型) - Web 面板:依赖管理、扫码登录、缓存清理、18 项运行时参数可调 安装 DSH 插件 dsh plugin --profile web add dsh-bili-watch 重启 DSH 后生效。
展开完整文档(README 剩余 1 节)
dsh-bili-watch

DSH 插件 / 命令行工具:把 B 站视频解析成模型能直接消费的结构化内容(元数据、评论、弹幕、AI 字幕、本地 ASR 转写、抽帧 OCR)。

为什么用它

- 本地跑不上传:ASR 转写 + OCR 抽帧全在本机,不依赖云 API、不花钱
- 防字幕诱饵:语速异常或字幕自称「实验/钓鱼」即判可疑;关键词命中率只在语速也异常时才作补充;可疑时加 --verify 抽 5 帧 OCR 比对画面(不加只提示一句)
- 二次读瞬时:缓存目录 + 文件锁,同一视频二次读取秒出
- 为 AI 设计:manifest 落盘 + 摘要输出,智能体直接消费不用再解析

特性

- 支持 b23.tv 短链、BV 号、av 号及通用 URL(经 yt-dlp)
- 降级链:AI 字幕 → 本地 ASR → 抽帧 OCR → 读图,四级全 miss 时退出码为 1
- 匿名可用:元数据、评论、弹幕、搜索、热榜无需登录
- 可选依赖:ffmpeg(抽帧)、Python venv(ASR/OCR)、faster-whisper(转写模型)
- Web 面板:依赖管理、扫码登录、缓存清理、18 项运行时参数可调

安装

DSH 插件

dsh plugin --profile web add dsh-bili-watch

重启 DSH 后生效。DSH ≥ 0.2.0-rc.2 自动写入 dsh.profile.bundles;更早版本需手动在 profile package.json 的 dsh.profile.bundles 数组追加 "dsh-bili-watch"。

装完重启即可用。 本插件的 bundle patch 不带 disabled,装进 profile 的 dsh.profile.bundles 后重启 DSH 就生效,命令直接存在。

0.3.1 及更早的版本不是这样:包在装的那一刻就被写进 dsh.profile.bundles(reconcile() 干的),所以插件页显示「已启用」,但实际不加载——因为层里那行 disabled: true 是插件页开关碰不到的位置。症状是「显示已启用但命令不存在,点开关也没用」。那个 disabled 是 0.2.17 应急修 bug 时误加的,带了 7 个版本,0.3.2 已去掉。

从旧版本升级的注意:reconcile() 只在「新增依赖」时跑,所以以前手动从 dsh.profile.bundles 删过包名的人,pnpm update 到 0.3.2 之后包名不会自动补回来,需要重跑一次 dsh plugin --profile web add dsh-bili-watch。

验证:dsh --profile web --dump-config 输出中可检索到 dsh-bili-watch。

亦可直接放入 profiles/<name>/node_modules/dsh-bili-watch。

插件依赖 @deepseek-ai/dsh-tools(peer, optional)。缺失时 bili_watch 工具不注册,/bili 命令与面板仍可用。

命令行全局安装

npm i -g dsh-bili-watch
bili-watch view BV1EPnG6DEJC

或 clone 后直接运行:

node tools/look.mjs view BV1EPnG6DEJC

用法

命令一览

| 命令 | 说明 |
| view <target> | 元数据 + 简介 + 热评。不下载、不依赖 ffmpeg,匿名可用 |
| peek <target> | 元数据 + 热评 + 5 帧画面。先取 B 站官方截帧接口,不下视频;取不到才回落下载 360p 抽帧(约 1 MB/min);OCR 环境就绪时顺带识别。仅 B 站 |
| sub <BV或链接> [--format srt\|json\|md\|txt] [--out <path>] | AI 字幕。--format 输出 SRT / JSON(含时间戳)/ Markdown(含跳转链接)/ 纯文本 txt。需登录 |
| comments <bv> [N] | 评论区,默认按热度排序 |
| asr <target> | 本地 ASR 转写,附「可信 / 疑似幻觉」判定 |
| frames <target> [--step <sec>] [--ocr] | 抽帧;--ocr 附带画面文字识别 |
| ocr <bv> | 抽帧并 OCR |
| text <target> [--format json\|md] | 四级降级链:字幕 → 转写 → 抽帧 OCR → 读图。全 miss 退出码 1 |
| sheet <bv> [--grid 3x3] | 均匀抽帧拼图,默认 9 帧 |
| substrip <bv> [--step <sec>] | 底部字幕竖拼 |
| danmaku <bv> | 弹幕热区(按时间桶统计峰值) |
| local <path> [--ocr] [--frames] | 本地视频文件走降级链(从转写层起)。仅命令行 |
| search <关键词> [--days 3] [--limit 10] [--order pubdate\|click] | 视频搜索。--days 生效时最多抓 3 页,结果仍按 --limit 截断 |
| hot [--limit 10] | 综合热门榜 |
| login [--check] [--poll] [--key <key>] | 扫码登录。--check 校验凭证有效性;--key 需配合 --poll |
| clean | 清空缓存根。任务进行中(锁文件存活)时拒绝并返回非零退出码 |
<target> 接受 BV 号、b23.tv 短链、av 号或 URL。

通用参数

| 参数 | 类型 | 默认值 | 说明 |
| --format | enum | — | 输出格式。text / sub / comments / danmaku / local 落盘时必须配合此参数,单独指定 --out 返回退出码 1 |
| --out <path> | string | — | 落盘路径,相对当前工作目录(cwd),目录不存在时自动创建。text / sub / comments / danmaku / local 写文件;frames 用它指定输出目录。sheet / ocr / substrip 固定写缓存目录,不收 --out |
| --ffmpeg <path> | string | — | ffmpeg 可执行文件路径 |
| --cred <path> | string | — | 凭证 JSON 路径,格式 {sessdata, bili_jct, buvid3, buvid4} |
| --asr-max <ms> | number | 1800000 | 转写时长闸门(毫秒)。视频长于此值时仅输出画面描述 |
| --head <n> | number | 0(按自动规则) | 只给前 n 字。不传或传 0 时按「正文超过 800 字只给开头 400 字 + 落盘路径」处理;要全文用 --full |
| --full | flag | — | 输出全文(不截断) |
| --verify | flag | — | 字幕可疑时抽 5 帧 OCR 交叉校验。字幕正常时不触发 |
| --fresh | flag | — | 忽略缓存强制重跑 |
| --mark-seen | flag | — | 标记为已读 |
| --no-time | flag | — | text/sub 默认输出带 [mm:ss] 逐句时间码,加这个退回纯文本(不带时间戳) |
| --at <时刻> | string | — | frames 精看某一刻,支持三种写法:750(秒)、12:30(分:秒)、12:30-13:10(区间) |

完整参数列表见 bili-watch help。

长文本保护

正文超过 800 字时,终端仅返回前 400 字摘要 + 落盘路径,全文写入缓存目录。工具层(bili_watch)阈值为 2000 字(摘要 800 字)。

面板

装进 DSH 后,在插件页点 dsh-bili-watch 卡片进详情页,里面有一块面板(不用先进设置),日常操作不用去对话里敲命令:

- 依赖状态:ffmpeg、yt-dlp、ASR 环境、OCR 环境、转写模型、B 站登录凭证逐项列出是否就绪、实际路径、占用体积。缺的点行末按钮装(走内置 fetch-deps,体积是估算值,实际以跑完为准)。下载期间要保持 DSH 开着——宿主退出会连下载进程一起结束,重进面板再点一次即可,已经下完的项会自动跳过。
- 扫码登录:未登录显示「去扫码登录」,已登录是「重新扫码」。二维码在面板里出,手机 B 站扫完自动刷新。凭证存在本机 $DSH_HOME/bili-watch/credential.json。
- 哪些能删:yt-dlp、转写模型、登录凭证可以直接删;ffmpeg / ASR 环境 / OCR 环境不给删除按钮——环境删了要重跑 fetch-deps 装回来,代价比省下的空间大,面板只在旁边标出实际位置。
- 批量操作:标题行的「批量下载 / 批量清理」打开勾选清单,删除前会再确认一次。
- 抽帧缓存:显示当前占用与文件数,「立即清除」一键清空;不点则按规则自动清理(保留天数、总量上限),超过从最旧的删起。规则存 deps/cache-config.json,启动时与打开面板时各查一次。
- 缓存位置:默认 $DSH_HOME/bili-watch/cache($DSH_HOME 是 DSH 家目录,Windows 下通常是用户目录里的 .dsh)。登录凭证是它的兄弟文件,清缓存时别把上一层一起删。换位置用环境变量 BILI_WATCH_CACHE,或 deps/deps-config.json 里的 "cache";指到还不是缓存根的目录时,插件会在里面建 bili-watch-cache/,不会直接拿你的目录当缓存根。clean 与面板发现旧位置还有东西、又没本工具的标记时会提示一句——本版不动那些文件,要清理得你自己删。
- 运行时参数:转写闸门、抽帧张数上限这类常改的放常规组,其余收在「高级设置」里(默认收起),每项都标了范围和「调大/调小会怎样」。
- 最近一次调用:显示命令、时间、成败,用来判断上一次是不是卡住了。

0.3.2 起装完重启就是启用状态,不用再去插件页手动打开。

配置

环境变量

| 变量 | 默认值 | 说明 |
| BILI_WATCH_PROJ | deps/ | ASR/OCR 环境目录 |
| BILI_WATCH_FFMPEG | — | ffmpeg 可执行文件路径 |
| BILI_WATCH_YTDLP | — | yt-dlp 可执行文件路径 |
| BILI_WATCH_SEEN | deps/seen.md | 已读标记文件路径 |
| BILI_WATCH_CACHE | $DSH_HOME/bili-watch/cache | 抽帧缓存根目录。优先级:环境变量 > deps/deps-config.json > 默认值 |
| BILI_WATCH_MIRROR | — | 设为 cn 时 fetch-deps 走国内镜像 |
| HF_ENDPOINT | https://hf-mirror.com | 转写模型下载端点 |
| BILI_ASR_MODEL | small | faster-whisper 模型名 |

运行时参数(面板可调)

以下参数通过 Web 面板或 deps/deps-config.json 调整,保存后立即生效:

| 参数 | 类型 | 默认值 | 说明 |
| asrMemMB | number (MB) | 1536 | 本地 ASR 内存门槛。低于此值跳过转写 |
| lockTtlMin | number (min) | 30 | 残留锁过期时长 |
| unitLimit | number (chars) | 5000 | 单条评论/弹幕截断阈值 |
| textLimit | number(字符数) | 800 | 文本层单次返回正文上限 |
| toolTextCap | number(字符数) | 2000 | 工具层单次返回正文总上限 |
| toolHead | number(字符数) | 800 | 工具层默认返回头部字数 |
| dmBucketSec | number(秒) | 30 | 弹幕分桶宽度 |
| dmTop | number(桶数) | 5 | 弹幕热区输出条数 |
| downloadMaxMB | number(MB) | 512 | 单文件下载体积上限 |
| subRateMin10 | number(×10 字符/秒) | 20 | 字幕语速下限(2.0 字/秒) |
| subRateMax10 | number(×10 字符/秒) | 85 | 字幕语速上限(8.5 字/秒) |
| textBrief | number(字符数) | 400 | 超长文本摘要长度 |
| asrMaxSec | number(秒) | 1800 | 转写时长闸门 |
| maxFrames | number | 500 | 抽帧张数上限 |
| asrTimeoutSec | number(秒) | 900 | ASR 进程超时 |
| ffmpegTimeoutSec | number(秒) | 300 | ffmpeg 进程超时 |
| cmdTimeoutSec | number(秒) | 180 | 通用命令超时 |
| asrDevice | enum | auto | ASR 推理设备:auto(有 CUDA 就用,失败回落 CPU)/ cpu(强制 CPU)。面板上是「转写时用 GPU」开关 |

缓存路径

默认缓存根:$DSH_HOME/bili-watch/cache($DSH_HOME 默认为用户目录下 .dsh)。

- 登录凭证:$DSH_HOME/bili-watch/credential.json(缓存根的兄弟文件,清理缓存时勿误删上级目录)
- 自定义缓存位置:设置 BILI_WATCH_CACHE 环境变量,或在 deps/deps-config.json 写入 "cache": "<dir>"。指向外部目录时插件自动创建 bili-watch-cache/ 子目录作为缓存根
- 修改后需重启 DSH 生效
- 自动清理规则(保留天数 / 总量上限)存于 deps/cache-config.json,默认保留 7 天 / 上限 2048 MB,启动时与打开面板时各检查一次
- 缓存根归插件专用:clean 与面板「立即清除」按名字整棵删这 10 个子目录 —— _logs frames sheets sheet-tmp substrip-tmp _misc asr frames-dl audio-dl _hot,里面的东西一律不保留。别把自己的文件放进这些名字的目录;放在缓存根根目录下、名字不在上表的文件不会被删(clean 会跳过并列进「不是 bili-watch 的缓存条目」)

卸载

dsh plugin --profile web remove dsh-bili-watch

卸载前备份

0.3.4 起依赖与配置都在 $DSH_HOME/bili-watch/deps(包外),卸载不会删掉。只有首次启动还没把包内 deps/ 搬完的用户(这时仍在用包内那份)才需要提前拷出以下 4 份文件,重装后放回原位:

1. deps/deps-config.json — 运行时参数与自定义缓存路径
2. deps/cache-config.json — 自动清理规则
3. deps/ui-config.json — 面板显示开关
4. deps/seen.md — 已读标记(或将 BILI_WATCH_SEEN 指向包外路径)

卸载后残留

- 登录凭证 $DSH_HOME/bili-watch/credential.json 不在包内,卸载不删除;如需清除手动删除
- 缓存根 $DSH_HOME/bili-watch/cache 不在包内,卸载不删除;仅清理 cache/ 子目录,勿删上级凭证文件

依赖

| 能力 | 依赖 | 缺失行为 |
| 元数据 / 评论 / 弹幕 / 搜索 / 热榜 | 无(匿名 HTTP) | — |
| 抽帧 / 九宫格 / 字幕带 | ffmpeg | 报错并提示安装方式 |
| AI 字幕 | B 站登录态(SESSDATA) | 返回无字幕轨,降级至下一级 |
| 本地 ASR | Python 3.10–3.14 + faster-whisper(deps/.venv-asr)+ VC++ 运行库 | 跳过,降级 |
| 画面 OCR | Python 3.10–3.14 + rapidocr_onnxruntime(deps/.venv-ocr)+ VC++ 运行库 | 跳过,仅基础抽帧 |
| 通用 URL | yt-dlp | 仅支持 B 站 |

环境要求:Node.js ≥ 22。

VC++ 运行库:ASR 与 OCR 用的 faster-whisper / onnxruntime 是 C++ 编译的 wheel,运行时要 vcruntime140.dll。Windows 10/11 一般自带,但精简过的企业镜像可能没有——症状是 pip 装得上、import 时报 DLL load failed while importing …。这时装一遍 Microsoft Visual C++ 2015–2022 Redistributable (x64),与 Python 版本无关,不必重装 venv。

ffmpeg 查找顺序:--ffmpeg > BILI_WATCH_FFMPEG > deps/deps-config.json > deps/ffmpeg.exe > PATH。

一键安装依赖

Windows
powershell -ExecutionPolicy Bypass -File scripts\fetch-deps.ps1 -Target all
powershell -ExecutionPolicy Bypass -File scripts\fetch-deps.ps1 -Target all -Mirror cn

单项:ffmpeg / ytdlp / asr / ocr / model

powershell -ExecutionPolicy Bypass -File scripts\fetch-deps.ps1 -Target ytdlp
bash

macOS / Linux

bash scripts/fetch-deps.sh all
BILI_WATCH_MIRROR=cn bash scripts/fetch-deps.sh all

依赖装在哪(0.3.4 起):默认放 $DSH_HOME/bili-watch/deps(DSH_HOME 默认 ~/.dsh),不在插件包目录里——这样 npm uninstall / 重装插件不会把两个 venv 和 460 MB 模型一起删掉。
从旧版升级时,首次启动会自动把包内 deps/ 搬过去(复制完成后才切换,中途失败继续用包内那份,面板会给出提示);包内旧的 deps/ 不再使用,确认新位置正常后可以手动删除。
想放到别的盘:设环境变量 BILI_WATCH_DEPS 指向目标目录即可(CLI、面板、一键脚本三处同源)。

GPU 加速(可选)

只有本地 ASR 转写用得上 GPU,OCR 走 onnxruntime 的 CPU 推理,与显卡无关。

| 你的机器 | 面板「GPU 运行库」一行 | 转写 |
| 有 NVIDIA 卡 + 已装运行库 | 绿点,显示 cublas64_12.dll | 走 GPU,比 CPU 快 2–8 倍 |
| 有 NVIDIA 卡 + 没装运行库 | 灰点,提示「装上快 2-8 倍」 | 走 CPU(功能一样,只是慢些) |
| 没有 NVIDIA 卡 | 灰点,说明没有 nvidia-smi | 走 CPU |

怎么装:面板上点那一行的「下载」,或命令行: powershell powershell -ExecutionPolicy Bypass -File scripts\fetch-deps.ps1 -Target cuda

装的是 pip 上的三个 wheel(nvidia-cublas-cu12 + nvidia-cudnn-cu12 + nvidia-cuda-nvrtc-cu12),约 500 MB,装在 ASR 的 venv 里,不需要装整套 CUDA Toolkit。装完脚本会真加载一次 cublas64_12.dll 校验——pip 装成功不代表 DLL 能加载(缺 VC++ Redist 时会 DLL load failed)。

为什么第一次慢:-Target all 不会自动装它(不问、不装 500 MB);首次转写还要加载模型到显存,比后续调用慢十几秒。

怎么关:面板「转写与抽帧」里的「转写时用 GPU」取消勾选(存的是 asrDevice: cpu),或把 deps/deps-config.json 里的 asrDevice 写成 cpu。删掉运行库也能自动退回 CPU,不会报错。

没有卡能不能开:能,但开了也走 CPU——auto 的语义是「有可用 CUDA 就用,没有或显存不足就回落 CPU」。

故障排查

凭证

仅 AI 字幕需要登录。凭证优先级:--cred > $DSH_HOME/bili-watch/credential.json > 匿名。

watch 只读自己这一份,不会去跟随同机其它工具写的凭证(旧版会按 mtime 挑最新的那份,容易出现「面板显示已退出、实际还在用别的身份」)。早期版本留下的旧位置凭证(deps/ 下那份)会在第一次读到时一次性复制到 $DSH_HOME/bili-watch/credential.json,之后不再回读。

凭证文件等同账号登录态,禁止外传或提交版本库。

转写可信度

本地 ASR 输出附判定字段:

- 可信:VAD 检测到足够人声,无跨窗重复模板句
- 疑似幻觉:VAD 人声 < 0.5 秒却输出文本、跨 2 秒以上窗口重复文本占比 > 50%、no_speech_prob > 0.5 分段占比过半

判为疑似幻觉时应通过抽帧 OCR 交叉验证,不可直接引用为台词。

降级链行为

AI 字幕(需登录) ↓ 无字幕轨本地 ASR(可选) ↓ 未安装 / 语速 < 2 字/秒抽帧 + OCR(可选) ↓ 未安装读图(模型直接看帧)

降级时输出原因,不静默跳过。四级全 miss 退出码 1。

字幕诱饵检测

text 命令自动检测以下可疑信号:

- 字幕语速不在 2–8.5 字/秒区间
- 字幕与标题 / UP 主关键词重合度过低(需与语速异常同时触发)
- 字幕包含「实验 / 钓鱼 / 测试」字样

指定 --verify 时额外抽 5 帧 OCR 比对画面文字。仅在可疑信号触发时生效,正常字幕不触发。

限制

- B 站侧只读:不支持点赞、评论、发弹幕、历史记录
- 通用 URL 走 yt-dlp 时不做内网地址过滤,禁止指向内网服务
- 无内置限速,批量调用需自行 sleep
- 工具名 bili_watch 与命令 /bili 若与其他插件冲突,后注册者覆盖前者

成本

- 字幕接口带一次重试;text 最多尝试两次(先匿名,有凭证再试一次)。
- 抽帧要下载视频(360p)再跑 ffmpeg,--step 调大能少抽;脚本另外限了 -threads 2,不会把机器吃满。
- 拼图(sheet)和字幕带(substrip)把多帧压成一张图给模型看,省 token。
- 本地转写、OCR 都在本机跑,不上传、不按次计费;只有取 B 站数据的接口走网络。

模块架构 mermaid
flowchart TD
subgraph L1["index.js(DSH 进程内)"]
DT["defineTool 注册 bili_watch"]
CR["commands.register 注册 /bili"]
WS["webServer 路由 / 面板"]
end
subgraph L2["tools/look.mjs(子进程)"]
LOOK["node tools/look.mjs <cmd>"]
end
subgraph L3["外部依赖(可选)"]
PY["deps/_bili-asr.py ASR"]
FF["ffmpeg 抽帧"]
YT["yt-dlp 下载"]
end
DT -->|spawn| LOOK
CR -->|spawn| LOOK
WS -.->|面板写凭证 / 内容命令 spawn| LOOK
LOOK -->|spawn| PY
LOOK -->|spawn| FF
LOOK -->|spawn| YT

致谢

- dsh-bilibili-video-summary(Aurum807):单链接自动提示形态
- dsh-bilibili-subtitles(Chenfan2):多格式导出形态
- video-watch-plus(MIT):ASR 幻觉证伪方法

License

MIT
预览分享卡片长什么样 ↓Sacredbluedragon/dsh-bili-watch
分享 · DeepSeek Harness Hub
Sacredbluedragon/dsh-bili-watch

给模型看 B 站视频的 DSH 插件:字幕、转写、抽帧 OCR、评论与弹幕;支持 b23.tv 短链,抽帧缓存目录可换。

回到顶部