dsh-bili-watch 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,6 +95,21 @@ node tools/look.mjs view BV1EPnG6DEJC
95
95
 
96
96
  正文超过 800 字时,终端仅返回前 400 字摘要 + 落盘路径,全文写入缓存目录。工具层(`bili_watch`)阈值为 2000 字(摘要 800 字)。
97
97
 
98
+ ## 面板
99
+
100
+ 装进 DSH 后,在插件页点 `dsh-bili-watch` 卡片进详情页,里面有一块面板(不用先进设置),日常操作不用去对话里敲命令:
101
+
102
+ - **依赖状态**:ffmpeg、yt-dlp、ASR 环境、OCR 环境、转写模型、B 站登录凭证逐项列出是否就绪、实际路径、占用体积。缺的点行末按钮后台装(走内置 `fetch-deps`,体积是估算值,实际以跑完为准)。
103
+ - **扫码登录**:未登录显示「去扫码登录」,已登录是「重新扫码」。二维码在面板里出,手机 B 站扫完自动刷新。凭证存在本机 `$DSH_HOME/bili-watch/credential.json`。
104
+ - **哪些能删**:yt-dlp、转写模型、登录凭证可以直接删;ffmpeg / ASR 环境 / OCR 环境不给删除按钮——环境删了要重跑 `fetch-deps` 装回来,代价比省下的空间大,面板只在旁边标出实际位置。
105
+ - **批量操作**:标题行的「批量下载 / 批量清理」打开勾选清单,删除前会再确认一次。
106
+ - **抽帧缓存**:显示当前占用与文件数,「立即清除」一键清空;不点则按规则自动清理(保留天数、总量上限),超过从最旧的删起。规则存 `deps/cache-config.json`,启动时与打开面板时各查一次。
107
+ - **缓存位置**:默认 `$DSH_HOME/bili-watch/cache`(`$DSH_HOME` 是 DSH 家目录,Windows 下通常是用户目录里的 `.dsh`)。登录凭证是它的兄弟文件,清缓存时别把上一层一起删。换位置用环境变量 `BILI_WATCH_CACHE`,或 `deps/deps-config.json` 里的 `"cache"`;指到还不是缓存根的目录时,插件会在里面建 `bili-watch-cache/`,不会直接拿你的目录当缓存根。
108
+ - **运行时参数**:转写闸门、抽帧张数上限这类常改的放常规组,其余收在「高级设置」里(默认收起),每项都标了范围和「调大/调小会怎样」。
109
+ - **最近一次调用**:显示命令、时间、成败,用来判断上一次是不是卡住了。
110
+
111
+ 0.3.2 起装完重启就是启用状态,不用再去插件页手动打开。
112
+
98
113
  ## 配置
99
114
 
100
115
  ### 环境变量
@@ -245,6 +260,13 @@ AI 字幕(需登录)
245
260
  - 无内置限速,批量调用需自行 sleep
246
261
  - 工具名 `bili_watch` 与命令 `/bili` 若与其他插件冲突,后注册者覆盖前者
247
262
 
263
+ ## 成本
264
+
265
+ - 字幕接口带一次重试;`text` 最多尝试两次(先匿名,有凭证再试一次)。
266
+ - 抽帧要下载视频(360p)再跑 ffmpeg,`--step` 调大能少抽;脚本另外限了 `-threads 2`,不会把机器吃满。
267
+ - 拼图(`sheet`)和字幕带(`substrip`)把多帧压成一张图给模型看,省 token。
268
+ - 本地转写、OCR 都在本机跑,不上传、不按次计费;只有取 B 站数据的接口走网络。
269
+
248
270
  ## 模块架构
249
271
 
250
272
  ```mermaid
package/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { spawn, execFileSync } from 'node:child_process'
13
13
  import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from 'node:fs'
14
14
  import { tmpdir, homedir } from 'node:os'
15
- import { dirname, join, relative } from 'node:path'
15
+ import { dirname, join, relative, basename } from 'node:path'
16
16
  import { fileURLToPath } from 'node:url'
17
17
  // @deepseek-ai/dsh-tools 是可选 peer:桌面版 autoInstallPeers:false,市场安装时不会自动装上。
18
18
  // 缺了只少一个 bili_watch 工具,/bili 命令、插件面板与路由照常工作,不能因此整块加载失败。
@@ -199,6 +199,35 @@ function readTuning() {
199
199
  for (const k of TUNING_KEYS) out[k] = tuneOne(k, cfg[k])
200
200
  return out
201
201
  }
202
+ /**
203
+ * 读 deps-config.json,坏文件先备份再退回默认值(X-16a,2026-10-28 WB-CN N12 §2 头号)。
204
+ *
205
+ * 原来两处写入口都是 `try { cur = JSON.parse(readFileSync(p)) } catch {}`:
206
+ * 解析失败 cur 变 `{}`,下次改**任意一个**参数就整份写回——ffmpeg 路径、cache 目录、
207
+ * proj、17 项调优参数全被擦掉。单写者就能触发,不需要并发,用户只会看到「配置莫名其妙没了」。
208
+ *
209
+ * 现在:文件不存在 → 默认值(正常);存在但坏了 → 改名成 `.broken-<ts>` 留档 + 默认值 + 一次告警。
210
+ * 留档而不是直接拒绝写,是因为拒绝写会让面板「保存」一直失败、用户也不知道该改哪个文件。
211
+ */
212
+ function readDepsCfgFile(p) {
213
+ let raw
214
+ try {
215
+ raw = readFileSync(p, 'utf8')
216
+ } catch (e) {
217
+ if (e?.code !== 'ENOENT') cfgWarning = `${basename(p)} 读取失败:${String(e?.message || e)}`
218
+ return {}
219
+ }
220
+ if (!raw.trim()) return {}
221
+ try {
222
+ const j = JSON.parse(raw)
223
+ return (j && typeof j === 'object' && !Array.isArray(j)) ? j : {}
224
+ } catch (e) {
225
+ const bak = `${p}.broken-${Date.now()}`
226
+ try { renameSync(p, bak) } catch {}
227
+ cfgWarning = `${basename(p)} 解析失败(${String(e?.message || e)}),原文件已备份为 ${basename(bak)},本次按默认值继续`
228
+ return {}
229
+ }
230
+ }
202
231
  /**
203
232
  * 参数分层专用写入口:一次读盘 → 按白名单改/删 → 整份写回。
204
233
  * 不能复用 writeDepsCfg:它是合并写,删键(面板「恢复默认」)在合并写里删不掉。
@@ -206,8 +235,7 @@ function readTuning() {
206
235
  */
207
236
  function writeTuning(body) {
208
237
  const p = join(DEPS, 'deps-config.json')
209
- let cur = {}
210
- try { cur = JSON.parse(readFileSync(p, 'utf8')) } catch {}
238
+ let cur = readDepsCfgFile(p)
211
239
  if (!cur || typeof cur !== 'object' || Array.isArray(cur)) cur = {}
212
240
  if (!body || typeof body !== 'object') return { ok: true, data: cur }
213
241
  let touched = false
@@ -510,7 +538,20 @@ function uiCfg() {
510
538
  try {
511
539
  const raw = JSON.parse(readFileSync(UI_CFG, 'utf8'))
512
540
  return { ...DEFAULT_UI_CFG, ...raw }
513
- } catch { return { ...DEFAULT_UI_CFG } }
541
+ } catch (e) {
542
+ // X-16a 同族(2026-10-28):原来坏文件静默回默认值,紧接着 writeUiCfg 就把坏文件覆盖掉——
543
+ // 用户手改坏的 4 个开关再也找不回来。这里先改名留档(rename 后文件就不存在了,不会重复备份)。
544
+ if (e?.code !== 'ENOENT') {
545
+ const bak = `${UI_CFG}.broken-${Date.now()}`
546
+ try {
547
+ renameSync(UI_CFG, bak)
548
+ cfgWarning = `ui-config.json 解析失败(${String(e?.message || e)}),原文件已备份为 ${basename(bak)},本次按默认值继续`
549
+ } catch (e2) {
550
+ cfgWarning = `ui-config.json 解析失败(${String(e?.message || e)})且备份失败(${String(e2?.message || e2)}),本次按默认值继续`
551
+ }
552
+ }
553
+ return { ...DEFAULT_UI_CFG }
554
+ }
514
555
  }
515
556
  function writeUiCfg(patch) {
516
557
  const cur = uiCfg()
@@ -862,17 +903,21 @@ function cacheCfg() {
862
903
  }
863
904
  }
864
905
 
906
+ // WB-CN N13(2026-10-28):原来吞异常、无返回值,三处调用都不看结果——「保存规则」写盘失败仍回
907
+ // 200 {ok:true},与 BLK-02 同族(BLK-02 只修了 tuning 那条)。现在把结果交出去,调用方按失败处理。
865
908
  function writeCacheCfg(days, maxMb) {
866
909
  try {
867
910
  writeFileSync(CACHE_CFG, JSON.stringify({ days, maxMb }), 'utf8')
868
- } catch {}
911
+ return { ok: true }
912
+ } catch (e) {
913
+ return { ok: false, why: String(e?.message || e) }
914
+ }
869
915
  }
870
916
 
871
917
  /** 合并写 deps\deps-config.json(只动指定字段,保留用户原有配置)。失败返回 {ok:false,error},不抛。 */
872
918
  function writeDepsCfg(patch) {
873
919
  const p = join(DEPS, 'deps-config.json')
874
- let cur = {}
875
- try { cur = JSON.parse(readFileSync(p, 'utf8')) } catch {}
920
+ const cur = readDepsCfgFile(p)
876
921
  const next = { ...cur, ...patch }
877
922
  try {
878
923
  writeFileSync(p, JSON.stringify(next, null, 2), 'utf8')
@@ -1717,10 +1762,11 @@ function apply(ctx) {
1717
1762
  if (req.method === 'POST' && url.pathname === '/api/bili-watch/cache/clear') {
1718
1763
  const body = await readJsonBody(req)
1719
1764
  if (Number.isFinite(body?.days) || Number.isFinite(body?.maxMb)) {
1720
- writeCacheCfg(
1765
+ const wc = writeCacheCfg(
1721
1766
  Number.isFinite(body?.days) ? Math.max(1, body.days) : cacheCfg().days,
1722
1767
  Number.isFinite(body?.maxMb) ? Math.max(100, body.maxMb) : cacheCfg().maxMb,
1723
1768
  )
1769
+ if (!wc.ok) return sendJson(res, 500, { ok: false, why: `清理规则保存失败:${wc.why || '未知错误'}` })
1724
1770
  }
1725
1771
  // A7(2026-10-27 批次8):先查锁再清理——frames 下若有正在跑的抽帧锁,直接拒绝,
1726
1772
  // 不再像以前那样「先删完再补一句 warning」(锁外删会跟运行中的抽帧抢目录)。
@@ -1779,16 +1825,19 @@ function apply(ctx) {
1779
1825
  // A2(2026-10-27 批次8):面板「保存规则」把 days/maxMb POST 到这里,以前被静默丢。
1780
1826
  // 落到 cache-config.json(与 /cache/clear 同源),不写进 ui-config.json。
1781
1827
  let rule = null
1828
+ let cacheErr = null
1782
1829
  if (Number.isFinite(body?.days) && body.days > 0) {
1783
1830
  const cur = cacheCfg()
1784
1831
  const days = Math.max(1, Math.floor(body.days))
1785
1832
  const maxMb = Number.isFinite(body?.maxMb) && body.maxMb > 0 ? Math.max(100, Math.floor(body.maxMb)) : cur.maxMb
1786
- writeCacheCfg(days, maxMb)
1833
+ const wc = writeCacheCfg(days, maxMb)
1834
+ if (!wc.ok) cacheErr = wc.why || '未知错误'
1787
1835
  rule = { days, maxMb }
1788
1836
  } else if (Number.isFinite(body?.maxMb) && body.maxMb > 0) {
1789
1837
  const cur = cacheCfg()
1790
1838
  const maxMb = Math.max(100, Math.floor(body.maxMb))
1791
- writeCacheCfg(cur.days, maxMb)
1839
+ const wc = writeCacheCfg(cur.days, maxMb)
1840
+ if (!wc.ok) cacheErr = wc.why || '未知错误'
1792
1841
  rule = { days: cur.days, maxMb }
1793
1842
  }
1794
1843
  // 参数分层(阶段B):五个白名单键写进 deps-config.json,表外的键忽略、不报错。
@@ -1799,6 +1848,7 @@ function apply(ctx) {
1799
1848
  if (!w.ok) tuneErr = w.why || '未知错误'
1800
1849
  }
1801
1850
  if (tuneErr) return sendJson(res, 500, { ok: false, why: `参数保存失败:${tuneErr}` })
1851
+ if (cacheErr) return sendJson(res, 500, { ok: false, why: `清理规则保存失败:${cacheErr}` })
1802
1852
  return sendJson(res, 200, { ok: true, data: { ...next, ...(rule || {}), tuning: readTuning() } })
1803
1853
  }
1804
1854
  sendJson(res, 404, { ok: false, why: 'not found' })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bili-watch",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "把 B 站视频读成文本给 DSH agent 用:元数据、评论、弹幕、字幕、本地转写、抽帧 OCR;支持 b23.tv 短链与 av 号,抽帧缓存目录可换。只读命令零依赖。",
6
6
  "type": "module",
package/tools/look.mjs CHANGED
@@ -1031,19 +1031,13 @@ async function grabFrames(bv, { step = 0, outDir, ffmpeg, fresh = false, p: page
1031
1031
  if (base !== f) continue // 含路径分隔符,拒绝
1032
1032
  try { unlinkSync(join(dir, f)) } catch {}
1033
1033
  }
1034
- } else if (m0) {
1035
- // 老 manifest:存在但没有 files 字段——按命名兜底删本工具产物(peek-NN / 纯数字帧 / ${bv}-NN.jpg)
1036
- const ownRe = new RegExp(`^${bv}-\\d+\\.jpg$`)
1037
- for (const f of jpgs()) {
1038
- if (/^peek-\d+\.jpg$/.test(f) || /^\d+\.jpg$/.test(f) || ownRe.test(f)) {
1039
- try { unlinkSync(join(dir, f)) } catch {}
1040
- }
1041
- }
1042
1034
  } else {
1043
- // A5(2026-10-27 批次8):连 manifest 都没有——说明这个目录不是本工具跑出来的缓存,
1044
- // 多半是用户自己的 --out 目录(里面可能放着用户自己的 1.jpg)。绝不能按命名猜着删,
1045
- // 一个都不碰,只打一行提示。
1046
- say('没有 manifest,跳过清理(目录里的文件一概不动)')
1035
+ // A5(2026-10-27 批次8)+ X-01(2026-10-28):没有 manifest,或 manifest 里没有 files 清单,
1036
+ // 都说明「这目录不是本工具这一次跑出来的」——绝不能按文件名猜着删。
1037
+ // 原来老 manifest(有文件、无 files)走命名兜底删 数字.jpg 与 peek-NN.jpg,
1038
+ // 用户的 1.jpg / 2.jpg 正好被第一条命中:`--fresh --out <用户目录>` 实测会把它们删掉。
1039
+ // 想在旧缓存上重抽,删掉整个缓存目录即可;这里一律不碰。
1040
+ say(m0 ? 'manifest 没有 files 清单,跳过清理(目录里的文件一概不动)' : '没有 manifest,跳过清理(目录里的文件一概不动)')
1047
1041
  }
1048
1042
  }
1049
1043