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

榨菜罐 Debuger-114514/dsh-bili-jar

DeepSeek Harnessv0.3.0
Debuger-1145145 天前有提交已提供中文文档扫描:含敏感能力

DSH 插件:右下角一只罐子,按你的兴趣轴去 B站 搜片、按互动率筛、让大模型当策展人挑五条并写理由

35.5/100
综合分
8
GitHub 星标
1,156
npm 周下载
—/5
用户评分

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

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

状态速览

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

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

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

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

✓npm 包dsh-bili-jar @ 0.3.0
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/29 04:17:55

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

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

读写/删除本地文件发起外部网络请求
查看扫描证据(共 96 处,展示前 50 条)
  • 写入本地文件
    index.js:164
    mkdirSync(dir, { recursive: true })
  • 读取本地文件
    index.js:180
    try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return fallback }
  • 读取本地文件
    index.js:193
    try { files = readdirSync(dir).filter((f) => f.endsWith('.md')).sort().reverse().slice(0, 6) } catch { continue }
  • 读取本地文件
    index.js:196
    try { txt = readFileSync(join(dir, f), 'utf8') } catch { continue }
  • 读取本地文件
    index.js:256
    try { return String(JSON.parse(readFileSync(join(PLUGIN_DIR, 'package.json'), 'utf8')).version || '') } catch { return '' }
  • 写入本地文件
    index.js:294
    try { writeFileSync(refreshFile, JSON.stringify(refreshAt), 'utf8') } catch { }
  • 读取本地文件
    index.js:311
    try { return JSON.parse(readFileSync(treeFile, 'utf8')) } catch { return {} }
  • 写入本地文件
    index.js:315
    try { writeFileSync(treeFile, JSON.stringify(userTree, null, 2), 'utf8') } catch (e) { logWarn('tree:save', String(e?.message ?? e)) }
  • 读取本地文件
    index.js:379
    feedback = readFileSync(feedbackFile, 'utf8').split(/ ? /).filter(Boolean)
  • 写入本地文件
    index.js:391
    try { writeFileSync(upsFile, JSON.stringify(list, 'utf8')) } catch { }
  • 写入本地文件
    index.js:567
    try { writeFileSync(shownFile, JSON.stringify(recentShown), 'utf8') } catch { }
  • 写入本地文件
    index.js:570
    try { writeFileSync(poolFile, JSON.stringify(prevPool), 'utf8') } catch (e) { log(`pool 落盘失败:${e?.message ?? e}`) }
  • 删除本地文件
    index.js:917
    try { if (existsSync(join(stateDir, 'avatar.png'))) rmSync(join(stateDir, 'avatar.png')) } catch { }
  • 删除本地文件
    index.js:941
    try { if (existsSync(join(stateDir, 'avatar.png'))) rmSync(join(stateDir, 'avatar.png')) } catch (e) {
  • 发起网络请求
    client.js:546
    const res = await fetch(API + '/history', {
  • 发起网络请求
    client.js:586
    const res = await fetch(API + '/more', {
  • 发起网络请求
    client.js:710
    const res = await fetch(API + '/explain?bvid=' + encodeURIComponent(it.bvid), { credentials: 'same-origin' })
  • 发起网络请求
    client.js:750
    const r = await fetch(API + '/later', {
  • 发起网络请求
    client.js:771
    await fetch(API + '/feedback', {
  • 发起网络请求
    client.js:793
    const res = await fetch(API + '/state', { credentials: 'same-origin', cache: 'no-store' })
  • 读取本地文件
    lib/hostdeps.mjs:49
    for (const n of readdirSync(join(npm, '@deepseek-ai'))) {
  • 读取本地文件
    lib/hostdeps.mjs:62
    for (const p of readdirSync(join(dshHome, 'profiles'))) {
  • 读取本地文件
    lib/hostdeps.mjs:77
    if (!existsSync(pkgFile)) return null
  • 读取本地文件
    lib/hostdeps.mjs:78
    const pkg = JSON.parse(readFileSync(pkgFile, 'utf8'))
  • 读取本地文件
    lib/hostdeps.mjs:85
    return existsSync(file) ? pathToFileURL(file).href : null
  • 读取本地文件
    lib/reasons.mjs:149
    try { return JSON.parse(readFileSync(file, 'utf8')) || {} } catch { return {} }
  • 写入本地文件
    lib/reasons.mjs:159
    writeFileSync(file, JSON.stringify(trimmed), 'utf8')
  • 写入本地文件
    lib/reasons.mjs:162
    writeFileSync(file, JSON.stringify(cache), 'utf8')
  • 读取本地文件
    lib/usage.mjs:138
    if (!existsSync(file)) return []
  • 读取本地文件
    lib/usage.mjs:139
    const raw = readFileSync(file, 'utf8')
  • 读取本地文件
    lib/usage.mjs:172
    if (existsSync(file)) {
  • 读取本地文件
    lib/usage.mjs:173
    const raw = readFileSync(file, 'utf8')
  • 写入本地文件
    lib/usage.mjs:176
    appendFileSync(file, lead + JSON.stringify(row) + '\n', 'utf8')
  • 读取本地文件
    lib/usage.mjs:220
    if (!existsSync(file)) return null
  • 读取本地文件
    lib/usage.mjs:221
    const raw = readFileSync(file, 'utf8')
  • 写入本地文件
    lib/usage.mjs:228
    writeFileSync(backup, raw, 'utf8')
  • 写入本地文件
    lib/usage.mjs:235
    writeFileSync(file, body ? body + '\n' : '', 'utf8')
  • 读取本地文件
    lib/statefile.mjs:16
    try { return JSON.parse(readFileSync(join(dir, name), 'utf8')) } catch { return dflt }
  • 读取本地文件
    lib/statefile.mjs:18
    const readText = (dir, name) => { try { return readFileSync(join(dir, name), 'utf8') } catch { return '' } }
  • 读取本地文件
    lib/statefile.mjs:19
    const sizeOf = (dir, name) => { try { return readFileSync(join(dir, name)).length } catch { return 0 } }
  • 读取本地文件
    lib/statefile.mjs:118
    for (const f of FILES) if (existsSync(join(dir, f))) w.push(`- \`${f}\` — ${kb(sizeOf(dir, f))}`)
  • 写入本地文件
    lib/statefile.mjs:128
    writeFileSync(out, w.join('\n'), 'utf8')
  • 发起网络请求
    lib/bili.mjs:47
    const res = await fetch(u, {
  • 请求外部主机www.bilibili.com
    lib/bili.mjs:346
    const res = await fetch(full, { headers: { 'user-agent': UA, referer: 'https://www.bilibili.com/' } })
  • 发起网络请求
    lib/bili.mjs:346
    const res = await fetch(full, { headers: { 'user-agent': UA, referer: 'https://www.bilibili.com/' } })
  • 写入本地文件
    lib/log.mjs:22
    appendFileSync(filePath, JSON.stringify(row) + '\n', 'utf8')
  • 读取本地文件
    lib/log.mjs:34
    if (!filePath || !existsSync(filePath)) return []
  • 读取本地文件
    lib/log.mjs:35
    const lines = readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean)
  • 写入本地文件
    lib/log.mjs:38
    writeFileSync(filePath, lines.slice(-MAX_LINES).join('\n') + '\n', 'utf8')
  • 发起网络请求
    lib/llm.mjs:24
    const res = await fetch(url, {
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
涉及外部主机:www.bilibili.com

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

依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-tools@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
v0.3.02026-10-28
上一版之后最主要的一件事:把「打开面板先空白 40 秒」修掉了。 · 不再空白 以前片单超过 30 分钟就阻塞重建,重开 DSH 后面板要对着空气等四十多秒。现在磁盘上有上一批就立刻给你看(实测 74ms),重建挪到后台;后台换好了只在顶部弹一行「新的一批装好了 · 点此查看」,点了才换 —— 不会把你正在读的卡片抽走。 · 三档刷新 不再是每 30 分钟跑一次全量 + 一次模型调用: | 距上次完整重建 | 做什么 | |---|---| | 不到 3 小时 | 不动 | | 3–24 小时(打开时)或满 6 小时(自动) | 轻量探测:6 个熟人的最新投稿 + 每根轴一次「最新发布」 | | 超过 24 小时 | 完整重建 | 轻量探测只在真捞到合格新片时才升级为完整重建,不叫模型、不算分。 · 熟人名单 从点赞、稍后看、点开过的记录里认人。同一条视频点三次只算一次,180 天前的互动不算。 · 1–2 次 = 观察中,3 次及以上 = 转正 · 转正之后他的新片(3 天内)直接进候选并加权 —— 靠预测,不等互动数据成熟 · 踩一次扣 2 次互动,扣到 0 以下拉黑 名单不占设置页,只在日志和 STATE.md 里报一行。 · 防回音室 片单里熟人来源最多 2 条(5 条时)。规则挑选和模型策展两条路径都收 —— 模型那条实测出现过多达 3/5。11 条单测守着。 · 修了什么 · 固定 UP 拉不到投稿:/space/wbi/arc/search 只做 WBI 签名会被风控挡回(游客态稳定 -352,偶尔直接返回 HTML 拦截页)。补浏览器指纹参数 + buvid4 + 全局 750ms 节流;失败时「关注」页会说人话 · 决赛圈查详情改 4 并发、字幕/评论改 3 并发 · history.json 补存 mid;feedback.jsonl 同一条视频只记一次(之前一条能记 3 行,把熟人分数算虚了) · 数字 · 首屏 40s+ → 74ms · 重建 ~43s → 16–21s(2 轴实测) · 每天 B站请求 ~210 → ~96 次;模型调用最多 10 → 1 次 · 单测 83 → 128 完整说明见 README 和 docs/ups-and-refresh.md。
v0.2.92026-10-28
· 这一版:让片单真的「新」起来 · 冷启动补偿 搜索接口不返回投币/收藏,新条目进池时互动分是 0,而详情补查只覆盖初评前 12 名 —— 新条目永远挤不进前 12、永远没数据。现在: · 每轮额外给 8 条「刚进池、缺数据」的补详情 · 缺数据且播放达标(≥5 万)、非标题党时,按年龄给 0.05 / 0.03 / 0.01 的临时补偿 · 补过数据就失效,补贴是临时的 · 新鲜度配额 main 里至少担保一条:≤3 天 + 播放达标 + 贴轴 + 非标题党。 分数调不动时由规则兜底,且不会把某根轴清空。 · 数据会过期 互动数据 7 天后重刷(每轮最多 3 条,播放高的先刷)。 历史条目自动补 statsAt,让老数据也能进入刷新队列。 · 播放增速 记上一次的播放量和时间,算出「每天涨多少」。涨得快的优先补数据,冷启动再加 0.01(总分仍封顶 0.05)。 · 多样性(新设置项) 分数排序太确定,同一个池子每次推的都一样。现在: · 前 5 名固定 · 其余名额按分数加权抽签(Gumbel 噪声) · 设置里「多样性 0–1」可调,0 = 完全确定 · 种子记在 stats.diversity,出问题能重放 · 测试 74 → 83 个用例,覆盖补数据门槛、配额替换、增速基准、抽样可复现。
v0.2.02026-10-23
右下角一只罐子,替你挑「今天看哪几条」B 站视频。 不用 B 站 的推荐流:按你自己的兴趣轴去搜,按互动率(赞/币/藏)而不是播放量排好坏, 攒进一个跨天的池子,最后让大模型当策展人挑五条、写一句讲内容不讲数据理由。 · 这一版有什么 · 预设轴树 30 根 / 68 个子簇:游戏 音乐 舞蹈 动画 知识 科技 影视 娱乐 生活 美食 运动 汽车 时尚 动物 鬼畜 纪录片 搞笑 学习 手工 摄影 历史 哲学 心理学 编程 数学 物理 军事 财经 职场 育儿 · 轴树会长:从池子里挖子簇(tag 的 DF + PMI,泛标签按 PMI 中位数判、跨轴去重、阈值自适应), 再让模型审核(判要不要 / 起中文名 / 说理由),设置页点一下才写进你自己的树 · 前端重做:插画面板、装罐时两帧走路、列表底部的小人、四档头像预设 · 赞/踩参与打分:攒够 20 条后生效(同 UP ±0.04/0.07,tag 小幅加减,上限压死) · 小窗播放:卡片上直接浮一个播放器,不跳走 · 语气预设:自带三套,支持一键导入 .md;头像四档可换 · 更多电子榨菜:不重新搜,从池子里再捞,推荐语优先用存过盘的 · 记账:按 DeepSeek 官方峰谷规则算钱(北京时间工作日 9–12 / 14–18 为峰),三层看 · 修掉的 · 峰谷计价按本机时区算,成本被算成一半(0.023 vs 平台 0.039) · /explain 的 ReferenceError(点了必 500) · /debug 把完整 API key 吐给浏览器 · explainOn 是个死开关;换头像后页头不刷新 · 走路帧用人工量的框切,把背包裁掉了 42px · 安装 dsh plugin --profile web add dsh-bili-jar 桌面版(desktop profile)由 Electron 独占,需手改 ~/.dsh/profiles/desktop/package.json 后 pnpm install,详见 README。 · 注意 · 插件会调用 DeepSeek API(你自己的 key),一轮「换一批」约 ¥0.004–0.01 · API key 存在 ~/.dsh/bili-jar/config.json(明文),HTTP 接口只返回掩码 · B站 登录是可选的,只用于取字幕;凭证只落在插件自己的目录
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
电子榨菜罐 · dsh-bili-jar 鲸鱼娘带着一个罐子,守在右下角。她会按你的兴趣去 B 站探险,按赞币藏率排,给你找一些对味的电子榨菜。 她平时睡着。点一下罐子,她就出门找,回来把五条装进罐子,再和你说说这些视频讲了什么。 面板图是用插件自己那套 CSS 渲染的,不是屏摄 —— 真机上长得一样,只是多了窗口背景。 先看四行: - 需要 dsh ≥ 0.1.7;装完要重启一次(插件在 bundle 层组装)。 - 需要 你自己的 DeepSeek API Key。一轮「换一批」大约 ¥0.004–0.01。 - 时政这类只留最近两周,游戏攻略和音乐不限时间 —— 每根轴她自己定。 - 第一次装要等一轮(二三十秒),之后每次点开都是秒开 —— 上一批一直在磁盘上。 它干什么 - 按你填的兴趣轴(deepseek / 时政 / 历史 / 随便什么)去搜 B 站 - 按互动率(赞、币、藏、弹幕除以播放)排好坏,而不是播放量 - 结果并进一个池子,跨天累积;今天没搜到的好东西,明天被别的词带出来还在 - 最后把前 24 条交给模型,让它挑 5 条,每条写一句理由 - 理由只说内容:讲了什么、怎么讲的、适合什么时候看。
展开完整文档(README 剩余 1 节)
电子榨菜罐 · dsh-bili-jar

鲸鱼娘带着一个罐子,守在右下角。她会按你的兴趣去 B 站探险,按赞币藏率排,给你找一些对味的电子榨菜。

她平时睡着。点一下罐子,她就出门找,回来把五条装进罐子,再和你说说这些视频讲了什么。

面板图是用插件自己那套 CSS 渲染的,不是屏摄 —— 真机上长得一样,只是多了窗口背景。

先看四行:

- 需要 dsh ≥ 0.1.7;装完要重启一次(插件在 bundle 层组装)。
- 需要 你自己的 DeepSeek API Key。一轮「换一批」大约 ¥0.004–0.01。
- 时政这类只留最近两周,游戏攻略和音乐不限时间 —— 每根轴她自己定。
- 第一次装要等一轮(二三十秒),之后每次点开都是秒开 —— 上一批一直在磁盘上。

它干什么

- 按你填的兴趣轴(deepseek / 时政 / 历史 / 随便什么)去搜 B 站
- 按互动率(赞、币、藏、弹幕除以播放)排好坏,而不是播放量
- 结果并进一个池子,跨天累积;今天没搜到的好东西,明天被别的词带出来还在
- 最后把前 24 条交给模型,让它挑 5 条,每条写一句理由
- 理由只说内容:讲了什么、怎么讲的、适合什么时候看。数据用来挑,不写进理由
- 点开卡片能小窗播放;不感兴趣的可以 👍/👎(攒够 20 条开始影响打分)
- 打开就有东西:上一批存在磁盘上,面板一开先给你看,重建在后台做。换好了只在顶部弹一行「新的一批装好了」,点了才换 —— 不会把你正在读的卡片抽走
- 认熟人:从你点过的赞、加的稍后看、点开过的视频里认人。1 次互动算观察,3 次转正;转正之后他发的新片会被优先拿来试,不等互动数据攒够
- 熟人最多占 5 条里的 2 条,剩下的必须走发现路径 —— 不然越用越窄,变成只看那几个人
- 点击标题、「打开」或「小窗」即记为已观看,从推荐、关注和待看中移除,小红点同步减少(只统计推荐列表)。「历史」页保留最近 10 条,重复观看移到最前,重启后仍保留;历史超过 10 条时移除最早记录,当前片单仍不会补回已看视频。

模型没配或调用失败时退回纯规则挑片:每根轴先挑一条相似度 ≥0.85 的,同轴最多 2 条、同 UP 最多 1 条。片单不会因为没模型就空着。

一轮都经过什么

搜 每根轴 3 个词(第 3 个按小时轮换):前两个走综合排序保相关,第 3 个走「最新发布」保新鲜熟人 你赞过 / 点开过 / 加过稍后看的 UP(最多 6 个)直接拉最新投稿,3 天内的进候选并加权补货 某根轴凑不够 3 条时,换两条通道再找:按时长带重搜(news 轴走最新、常青轴走综合)+ 从种子扩散(相关推荐,30 天内的新种子优先)并池 和上一轮的池子合并,同一个 BV 只更新数据;池子上限 400,一直没再露面的先淘汰筛 时长、播放门槛、充电专属、直播回放、日期窗口、贴不上任何轴 —— 全砍打分 两套权重各自合计 1.00:news 轴的时效占 0.10,evergreen 轴只占 0.02(差额给了播放 +0.03 和小众 tag +0.05);其余一样:赞/币/藏的率 0.39 + 贴轴 0.30 + 字幕 0.06 + 多轴 0.04 扣分 进过 main 的条目每轮 -0.03(封顶 0.15),把位置让给没见过的打乱 前 5 名固定,其余按分数加权抽签(设置里「多样性」0–1);种子记进 stats,能重放补查 前 12 名回查真实投币数(搜索接口给的是旧的),重新打分补漏 新条目缺互动数据时每轮补查 8 条(播放涨得快的优先),回查前按冷启动给 0.01–0.05 的临时加分;补过的数据 7 天后过期,每轮再刷 3 条配额 main 里至少一条「3 天内 + 播放达标 + 贴轴 + 非标题党」,没有就从合格候选里换一条进来(只换轴上还有队友的最低分)回音室 熟人来源最多 2 条 —— 规则挑完收一次,模型策展完再收一次策展 只把前 24 名交给模型(每条带上「发布几天前」和数据是否已回查),让它挑 5 条、写理由

为什么只给模型 24 条:给整个池子也能挑,但一次调用会贵十倍,挑得还未必更好。

「换一批」会排除最近 5 轮推过的所有条目(≤25 条)+ 已观看的 —— 之前只排除上一轮那 5 条,池子里的老面孔每轮都回来。

多久换一次

不是每半小时就在后台重跑一遍 —— 那样既慢(一次完整重建二三十秒)又费钱(每次都要叫一次模型)。分三档:

| 距上次完整重建 | 做什么 | 代价 |
| 不到 3 小时 | 不动,直接用手里这批 | 0 |
| 3–24 小时(你打开面板时)或满 6 小时(不管开不开) | 轻量探测:6 个熟人的最新投稿 + 每根轴一次「最新发布」 | 8–14 次请求,不叫模型、不算分 |
| 超过 24 小时 | 完整重建,就是上面那一整套 | 一次全量 + 一次模型调用 |

轻量探测只在真捞到「够新 + 有人看 + 贴轴」的片子时才升级成完整重建;捞不到就继续用手里这批。所以「一直没变」通常是正常的 —— 两小时内本来就不会凭空多出好片。

探测过一次,两个计时一起清零,不会重复触发。想看时间戳:~/.dsh/bili-jar/refresh.json,人话版在 STATE.md 里。

熟人是怎么认出来的

只用你自己的本地记录,不联网查你的账号:

| 来源 | 权重 |
| 👍 点赞 | 1 次互动 |
| 稍后看 | 1 次互动 |
| 点开看过 | 1 次互动 |
| 👎 踩 | 扣 2 次 |

- 同一条视频点三次也只算一次;180 天前的互动不算
- 攒到 1–2 次 = 观察中,3 次及以上 = 转正
- 踩扣到 0 以下 = 拉黑,不再拉他的投稿

转正之后,他的新片(3 天内)会直接进候选,并且加一点权重 —— 这是预测,不等点赞率、投币率攒成形。观察中的只加半个权重。

名单不占设置页的地方,只在日志(ups:list)和 STATE.md 里报一行。想重置就删 ~/.dsh/bili-jar/ups.json,下次自己重新长。

日期:每根轴自己决定

同一个窗口不可能同时适合「时政」和「空洞骑士攻略」—— 前者 14 天就过期,后者 2023 年的攻略今天照样有用。

所以每根轴有一个 freshness:

| | 日期窗口 | 时效分权重 |
| news | 14 天(全局可改) | 0.10 |
| evergreen | 不限 | 0.02 |

怎么定的:不看条数,看合格条目的年龄中位数。超过 90 天就算常青内容。

实测七个冷门词,分得很干净:丝之歌 361 天、空洞骑士 2052 天、vtuber 2385 天(都是 evergreen);时政 13 天、深蓝 26 天、术力口 5 天、kards 5 天(都是 news)。中间几乎没有模糊地带。

条数会跟着搜索结果抖(同一个词两次跑,命中数会变),中位数不会 —— 所以拿它当判据。

新轴默认 news,跑一轮后自动看:如果它凑不够 3 条、而且合格条目普遍很老,就在同一轮里改判成 evergreen,日志记一行原因。设置页每根轴旁边有个两态小开关,点了就是手填,手填之后不再被自动覆盖。

冷门轴为什么搜不到

搜「空洞骑士」时,B 站 的前 3 页被续作「丝之歌」的新内容占满,五年前的好视频排不进来 —— 有一条 33 分钟、108 万播放的攻略要翻到第 4 页才有。

所以凑不够 3 条时会走两条补充通道:

1. 按时长带重搜:B 站 搜索支持 duration 参数(1=<10 分 / 2=10–30 / 3=30–60 / 4=>60 分),一页 20 条全落在想要的时长里,比混着搜再筛掉强得多
2. 种子扩散:/x/web-interface/archive/related?bvid= 是按内容相似度给的,不受新内容淹没。从任意 3 条同主题视频出发(不需要它们合格),去重后能捞回几十条,其中就有搜不到的那条

两条通道都是纯 HTTP,不花模型钱。

小众 tag 分:一条视频的 tag 在池子里越罕见,这一项越高(取最罕见的三个 tag 平均)。目的是别让「必剪创作 / 高能」这种大标签把长尾内容挤下去。

预设

装好就有,不用配:

| 类型 | 自带 |
| 兴趣轴 | 30 根:游戏 / 音乐 / 舞蹈 / 动画 / 知识 / 科技 / 影视 / 娱乐 / 生活 / 美食 / 运动 / 汽车 / 时尚 / 动物 / 鬼畜 / 纪录片 / 搞笑 / 学习 / 手工 / 摄影 / 历史 / 哲学 / 心理学 / 编程 / 数学 / 物理 / 军事 / 财经 / 职场 / 育儿 |
| 子簇 | 68 个,比如「游戏 → 沙盒游戏 / 游戏推荐 / 独立游戏」 |
| 语气 | 三套:鲸鱼女仆 / 正经简报 / 损友吐槽 |
| 头像 | 五张:睡觉 / 看电视 / 探险 / 哦鲸鲸 / 走路(唯一会动的一张) |

清单在 docs/presets.md。设置页点「+ 游戏」就把整根轴连子簇加进来。

轴树会长。 如果预设里没有你要的,或者不够细,点「看建议」:

1. 从你池子里的 tag 挖候选(DF + 两两共现 PMI;跟谁都不特别的高频词会被当泛标签丢掉)
2. 模型再过一遍:判是不是真细分方向、是不是别的轴的词、起个能当标签用的名字、说一句为什么
3. 你点一下才写进自己的树(存在 ~/.dsh/bili-jar/axis-tree.json,不碰出厂值)

实跑过的例子:历史 挖出「人文历史档案馆 / 人文 / 美国」,模型全否了,理由是「UP 主品牌不是分类」「太泛」「属于时政轴」;deepseek 留了三个,把 ai编程 改成 AI 编程,把 claude 当作别轴的词丢掉。

记账

三层:单次 / 今天 / 累计。按 DeepSeek 官方规则算钱 —— 北京时间工作日 09:00–12:00、14:00–18:00 是峰时,其余(含周末和法定节假日)半价。

| 元 / 百万 token | 缓存命中 | 未命中 | 输出 |
| 峰时 | 0.04 | 2 | 8 |
| 谷时 | 0.02 | 1 | 4 |

历史记录按每条自己的时间戳重算,改规则会把旧账一起修正。

装

桌面版的 desktop profile 不让 CLI 装插件,只能手改依赖:

// ~/.dsh/profiles/desktop/package.json
{
"dependencies": { "dsh-bili-jar": "link:D:/你的路径/电子榨菜罐" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "dsh-bili-jar"] } }
}

cd ~/.dsh/profiles/desktop && pnpm install

其他 profile:

dsh plugin --profile web add dsh-bili-jar

装完重启 dsh,再刷新页面。

更新

npm 上的最新版看这里:<https://www.npmjs.com/package/dsh-bili-jar>

走 npm 装的
pnpm up dsh-bili-jar@latest

走 GitHub 装的(会重新拉默认分支)
pnpm up dsh-bili-jar

桌面版(依赖写死在 profile 里时)把 package.json 里的版本改成 ^0.3.0,或者直接写 @latest,再 pnpm install

装完重启 dsh,然后 Ctrl+Shift+R 硬刷新页面 —— 客户端脚本会被浏览器缓存,光点刷新不够。

怎么确认版本对不对:开一次面板,设置 页底部有版本号;或者直接看接口:

curl -s http://127.0.0.1:你的端口/api/bili-jar/ping | head -c 120

第一次用

1. 点罐子 → 设置 → 填 API Key(DeepSeek 官方)
2. 改兴趣轴。默认那四根是我的口味,换成你的
3. 回「推荐」,点换一批

第一批装好之后,以后每次点开罐子都是立刻有东西(上一次那批),后台再慢慢换。

熟人名单是空着起步的:👍 攒到 3 次、或者同一个人的视频点开过 3 次,他就转正了。头几天名单是空的,属于正常。

B 站登录是可选的,只用来取 AI 字幕。二维码是插件自己画的(lib/qr.mjs,零依赖),凭证落在自己的目录里。
不过「关注」页(固定 UP)和熟人投稿都依赖它 —— 没登录时拉不到投稿,登录过期了「关注」页会提示你重新扫码。

数据都在哪

~/.dsh/bili-jar/

| 文件 | 内容 |
| config.json | 面板里的设置 + 语气预设(含明文 API Key) |
| feed.json / pool.json | 当前片单 / 池子 |
| reasons.json | 攒下来的推荐理由,关掉再开还能用 |
| usage.json | 每次调用的 token 和费用 |
| feedback.jsonl / later.json | 赞踩 / 待看 |
| history.json | 最近 10 条观看记录,最新在前 |
| credential.json | B 站登录,只有你自己扫码才有 |
| ups.json | 熟人名单(认出来的 UP + 各自攒了几次互动) |
| refresh.json | 上次轻量探测 / 上次完整重建的时间 |
| STATE.md | 人话摘要:现在什么配置、这一批推了什么、花了多少 |

安全

做过一次检查,结论都写在这:

- 只监听 127.0.0.1(DSH 的 webServer 本来就这么绑),局域网访问不到
- 跨站请求一律拒:带 Sec-Fetch-Site: cross-site,或者 Origin 不是本机的,直接 403。避免你开着插件逛网页时,被某个页面偷偷改配置或触发付费调用
- API Key 明文存在 config.json,但所有 HTTP 接口只返回 sk-••••后四位,/debug 也一样
- B 站 cookie 存在 credential.json,只在你自己扫码后写入;POSIX 下用 0600
- 不读别的插件的凭证,除非你在设置里显式打开「复用刷B站的登录」(默认关)
- 不收集遥测,不往作者的任何服务器发数据。出站只有两处:bilibili.com 和你配的模型 API
- 小窗播放是 player.bilibili.com 的 iframe,等于在页面里跑 B 站的播放器

已知的限制

- 时政区很多视频没有 tag,只能靠标题里的实体词判断。「时政」这个词在 B 站被考公内容占着,真·国际时政得用实体词搜(见 docs/tag-tree.md)
- 赞踩的学习很粗:只看 UP 和 tag,不看内容本身。熟人名单也一样 —— 它只知道「你点过他三次」,不知道你为什么点
- 熟人要攒到 3 次互动才转正,新装的人头几天名单是空的;拉黑了就只能删 ups.json 重来
- 「关注」页和熟人投稿依赖 B 站登录。凭证不会自己续,过期后那两处就空了(会给提示,但得你重新扫码)
- 轻量探测的判据比主流程松(播放只要求 30%),偶尔会捞到一条其实不咋样的新片 —— 它只负责「值不值得跑一次全量」,最终还得过打分那一关
- 越往后捞越靠近池子边缘。「更多电子榨菜」按互动率排队,不像首推那样经过模型
- 预设树里有些子簇的词不干净(带 UP 名或活动名),个别大区(科技 / 音乐)只挖出 1–2 个
- 没有浏览器端的自动化测试,只有 tools/ 里那些自检脚本;UI 改动得自己看一遍

结构

| 文件 | 干什么 |
| index.js | host 半端:配置、HTTP 路由、池子落盘、策展接线 |
| client.js | 浏览器半端:罐子 + 面板 |
| lib/bili.mjs | B 站客户端:WBI 签名 / 搜索 / 详情 / 粉丝 / 投稿 / 字幕 |
| lib/pool.mjs | 候选池:轴匹配、硬过滤、打分、按轴轮转、轻量探测(lightProbe)、防回音室 |
| lib/ups.mjs | 熟人名单:从赞/稍后看/点开里认人,纯函数 |
| lib/refresh.mjs | 三档刷新:3h / 6h / 24h 该干什么,纯函数 |
| lib/tree.mjs | 轴树:内置 + 用户自己长的枝;子簇挖掘 |
| lib/axis-presets.mjs | 30 根预设轴(工具生成,别手改) |
| lib/tree-review.mjs | 让模型审子簇建议 |
| lib/curator.mjs | 让模型挑片并写理由 |
| lib/reasons.mjs | 写理由(API 直连 → 宿主模型兜底),带磁盘缓存 |
| lib/usage.mjs | token 记账 + 峰谷计价 |
| lib/qr.mjs | 零依赖 QR 编码器 |
| lib/statefile.mjs | 生成 STATE.md |
| lib/hostdeps.mjs | 从宿主树里取 @deepseek-ai/ |

工具

node tools/why.mjs BV1xxxxx # 这条为什么(没)进罐子?在不在池 / 打分明细 / 排第几 / 卡哪一关
node tools/feed.mjs 10000 0 # 跑一次建池,打印片单
node tools/tree-suggest.mjs 历史 # 从池子里挖子簇建议
node tools/tree-review.mjs deepseek # 规则 + 模型审一遍(--apply 才写盘)
node tools/qr-check.mjs # QR 编码器与 Python qrcode 逐位比对
node tools/affinity-check.mjs # 赞踩加权的单测
node tools/ups-check.mjs # 熟人名单的单测(去重 / 时间窗 / 三档 / 上限)
node tools/refresh-check.mjs # 三档刷新的单测(3h / 6h / 24h / 边界)
node tools/echo-check.mjs # 防回音室的单测(规则 + 策展两条路径)
node tools/up-probe.mjs # 固定 UP 拉不到投稿时,四种写法逐个对照
node tools/rl-probe.mjs 10 # 连打 space 接口,看多少下会被风控
node tools/pack-check.mjs # 发版前自检
python tools/cutout.py in.png out.png # 抠掉画在图里的棋盘格
python tools/sprites.py # 从设定表切走路帧

发版

pnpm check # 自检
npm version patch
git push --follow-tags
推 tag 会触发 .github/workflows/publish.yml 发 npm。细节在 docs/publish.md。

更新日志

0.3.0

- 不再空白:以前重开 DSH,面板要对着空气等 40 多秒(片单超过 30 分钟就阻塞重建)。现在磁盘上有旧片单就立刻给你看,重建挪到后台;后台换好了只在顶部弹一行「新的一批装好了」,点了才换,不会把你正在读的卡片抽走
- 快了:决赛圈查详情改 4 并发、5 条字幕/评论改 3 并发(原来一条一条排队等)
- 三档刷新:0–3 小时不动 / 超过 3 小时(你打开时)或 6 小时(自动)跑一次轻量探测 / 满 24 小时才完整重建。以前是每 30 分钟一次全量搜索 + 一次模型调用
- 熟人名单:从点赞、稍后看、点开过的记录里认出「你自己选出来的人」。1 次互动 = 观察中,3 次 = 熟人。熟人的新片(3 天内)直接进候选并加权,靠的是「预测」而不是等互动数据成熟
- 防回音室:片单里熟人来源最多 2 条(5 条时)。规则挑选和模型策展两条路径都收(模型那条曾实测挑出 3/5),11 条单测守着
- 修固定 UP 拉不到投稿:/space/wbi/arc/search 只做 WBI 签名会被风控挡回(游客态稳定 -352,偶尔直接返回 HTML 拦截页)。补上浏览器指纹参数 + buvid4 + 全局 750ms 节流;拉失败时「关注」页会说人话,不再干瞪眼
- history.json 补存 mid;feedback.jsonl 同一条视频只记一次(之前一条能记 3 行,把熟人分数算虚了)
- 测试 83 → 123(新增熟人名单 / 三档刷新 / 回音室三组)

0.2.8

- 修「新片永远排不进前 24」的补查闭环:搜索接口拿不到币/藏 → 新条目互动分垫底 → 只有初评前 12 名才回查详情 → 永远没数据。现在每轮先给 8 条贴轴新条目回查真数据(播放涨得快的优先),互动数据 7 天后自动刷新(每轮 3 条,播放高的先刷)
- 缺数据的条目在回查前按冷启动给一点临时加分(≤0.05,带 5 万播放门槛和标题党否决,补到真数据就撤);策展候选行标注「数据:未补」,模型不再把没回查当成没人看
- 新鲜度配额兜底:main 里至少一条「3 天内 + 播放达标 + 贴轴」的新片,替换只动「轴上还有队友」的最低分
- 条目新增 playGrowth(播放增速),参与补查排序和冷启动微调;stats 新增 detailCold / detailRefresh / coldStart / freshQuota / growthSeen
- 测试 38 → 62;真实 8 轴配置实测:近 4 天进前 24 从 0 → 4,main ≤4 天从 0 → 3

0.2.9

- 新增「多样性」:分数排序太确定,同一个池子每次推的都一样。现在前 5 名固定,剩下的按分数加权抽签;设置里 0–1 可调(0 = 老行为),种子记在 stats.diversity 里,出问题能重放
- 冷启动补数据(每轮 8 条)+ 缺互动数据时的临时加分(≤0.05,补过详情就失效)
- 新鲜度配额:main 里至少担保一条 3 天内、播放达标、贴轴的
- 互动数据 7 天过期重刷;播放增速用于「先补谁的数据」

0.2.7

- 修「总在推旧内容」:检索混血(每轴第 3 个词走「最新发布」,news 轴补货也走最新)、evergreen 时效权重兑现 0.02、池子上限 400 + 进过片单的逐轮扣分、排除集扩到最近 5 轮、常青改判加防护(近两周还有新内容的轴不许改判)、策展人能看到发布日期、缺日期不再当「很老」判死
- stats 新增 byOrder(两种排序各搜了几次)和 fresh(池子里 7 天内发布的条数)
- tools/why.mjs 升级:在不在池里、打分明细、第几名、卡在哪一关,一次看全
- 测试 20 → 38:新增打分权重 / 池子代谢 / shown 格式三组单测

0.2.6

- 介绍重写:主语从「一个罐子」换成「鲸鱼娘带着一个罐子」
- 首页换成面板实拍 + 走路动图;仓库里加了 screenshots.json,市场详情页会自动抓这两张

0.2.5

- 头像多一张「走路」:拿原来那套两帧走路做的,页头 AI 的头像会动
- 装罐时列表底下那个小人也会迈步(同一套两帧),跟廊道里出门找的不是同一个
- README 顶图换成新的「出门找 → 满载而归」

0.2.4

- 每根轴自己决定看不看老内容(freshness):按合格条目的年龄中位数判定,超过 90 天就算常青,设置里点一下就能改
- 冷门轴补货:按时长带重搜 + 从「相关推荐」做种子扩散。搜不到的老片能捞回来(实测「空洞骑士」从 0 条到 19 条)
- 加载动画改成「出门找 → 满载而归」:12 帧、底部廊道、一步一步走,角度和步幅都调小了
- 刷新时不再清空旧片单:旧卡片留在原位,走路动画表示正在更新

0.2.x

- 预设轴树 30 根 / 68 个子簇;子簇可以从池子里长,模型审过再采纳
- 前端重做:插画面板、装罐时走路、列表底部的小人、四档头像
- 赞踩参与打分;小窗播放;语气预设支持导入 .md
- 修:峰谷按本机时区算(成本算成一半)、/explain 的 ReferenceError、/debug 泄露 key、explainOn 死开关、换头像不刷新页头、走路帧被裁掉背包、设置页打不开(漏声明)、头像上传被 64KB 限制挡住
- 安全:拒跨站请求、响应加 nosniff、凭证文件 0600

0.1.0 第一版:按轴搜索、互动率打分、累积池、策展、扫码登录、记账。

许可

代码 MIT。插画(assets/jar-.png / av-.png / walk-.png)由作者用豆包生成,随仓库提供,只用于这个插件的界面。「哦鲸鲸」那档头像取自 dsh-whale-widget(MIT),详见 NOTICE。
预览分享卡片长什么样 ↓Debuger-114514/dsh-bili-jar
分享 · DeepSeek Harness Hub
Debuger-114514/dsh-bili-jar

DSH 插件:右下角一只罐子,按你的兴趣轴去 B站 搜片、按互动率筛、让大模型当策展人挑五条并写理由

回到顶部