dsh-bili-watch 0.3.0 → 0.3.2
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 +195 -196
- package/client.js +74 -29
- package/cordis.patch.yml +2 -1
- package/deps/_bili-asr.py +3 -1
- package/index.js +102 -33
- package/package.json +1 -1
- package/scripts/fetch-deps.ps1 +5 -3
- package/tools/look.mjs +94 -42
package/client.js
CHANGED
|
@@ -101,6 +101,23 @@ window.__ModuleLoader__.load({
|
|
|
101
101
|
|
|
102
102
|
// 参数分层(2026-10-28 阶段B):一项 = 输入框 + 默认值提示 + 范围 + 一句话后果 + 恢复默认。
|
|
103
103
|
// 只写 h(),不引新依赖;保存/恢复都由外面传进来的回调走 /config。
|
|
104
|
+
// 2026-10-28 老大要求「高级设置放开」:这 8 项原来是代码里的硬编码,现在都能在面板里调。
|
|
105
|
+
// 元数据跟后端 TUNING_DEFS 一一对应(键名、默认值、上下界都从 /status 的 tuningDefaults/tuningRanges 取,
|
|
106
|
+
// 这里只放文案),加项时后端 TUNING_DEFS 与这张表各加一行即可。
|
|
107
|
+
const ADV_TUNING = [
|
|
108
|
+
{ key: 'asrMemMB', label: '转写内存门槛', unit: 'MB', def: '1536', hint: '该值用于控制本地转写的内存门槛。可用内存低于此值时跳过转写、降级至字幕或抽帧;增大后更早放弃转写,减小后内存紧张时仍强制执行可能崩溃。默认 1536 MB。' },
|
|
109
|
+
{ key: 'lockTtlMin', label: '残留锁过期', unit: '分钟', def: '30', hint: '该值用于控制残留锁的过期时长。进程异常退出后锁文件超过此时长视为过期,新任务可接管;缩短后运行中的长转写可能被误判过期,延长后残留锁阻塞更久。默认 30 分钟。' },
|
|
110
|
+
{ key: 'unitLimit', label: '单条内容上限', unit: '字', def: '5000', hint: '该值用于控制单条评论或弹幕的截断阈值。超出时截断并标注省略字数;增大后内容更完整但占用更多上下文,减小后省上下文但长评论截断更短。默认 5000 字。' },
|
|
111
|
+
{ key: 'textLimit', label: '文本层默认字数', unit: '字', def: '800', hint: '该值用于控制文本层单次返回正文上限。超出时仅返回开头摘要与落盘路径;增大后对话内正文更长但占用更多上下文,减小后更早落盘。默认 800 字。' },
|
|
112
|
+
{ key: 'toolTextCap', label: '工具层总字数', unit: '字', def: '2000', hint: '该值用于控制工具层单次返回正文总上限。增大后每次工具返回的正文更长,占用更多对话上下文;减小后更早落盘。默认 2000 字。' },
|
|
113
|
+
{ key: 'toolHead', label: '工具层默认头部', unit: '字', def: '800', hint: '该值用于控制工具层默认返回的正文头部字数,超出部分写入落盘文件。增大后对话内暴露更多开头内容,减小后更早切换至落盘路径。默认 800 字。' },
|
|
114
|
+
{ key: 'dmBucketSec', label: '弹幕分桶宽度', unit: '秒', def: '30', hint: '该值用于控制弹幕热区的分桶时长。增大后热区粒度更粗、覆盖更长时段;减小后定位更精确但可能分散。默认 30 秒。' },
|
|
115
|
+
{ key: 'dmTop', label: '弹幕热区条数', unit: '桶', def: '5', hint: '该值用于控制弹幕热区输出的桶数。增大后列出更多时段但输出更长;减小后仅保留最突出时段。默认 5 桶。' },
|
|
116
|
+
{ key: 'downloadMaxMB', label: '单文件下载上限', unit: 'MB', def: '512', hint: '该值用于控制单个媒体文件的下载体积上限。超出时下载中止,需增大后重试;增大后允许更大文件但增加磁盘与带宽占用,减小后拒绝大文件。默认 512 MB。' },
|
|
117
|
+
{ key: 'subRateMin10', label: '字幕语速下限 ×10', unit: '(20=2.0 字/秒)', def: '20', hint: '该值用于控制字幕语速下限(×10,即 2.0 字/秒),低于此值判定为「字幕可疑」。减小后慢口播更不易误判但可能漏检诱饵,增大后慢口播更易误判。默认 2.0 字/秒。' },
|
|
118
|
+
{ key: 'subRateMax10', label: '字幕语速上限 ×10', unit: '(85=8.5 字/秒)', def: '85', hint: '该值用于控制字幕语速上限(×10,即 8.5 字/秒),高于此值判定为「字幕可疑」。增大后快语速更不易误判但可能漏检,减小后快语速更易误判。默认 8.5 字/秒。' },
|
|
119
|
+
{ key: 'textBrief', label: '超长文本摘要字数', unit: '字', def: '400', hint: '该值用于控制超长文本的摘要长度。正文超过「文本层默认字数」时,对话内仅返回开头这么多字,全文落盘;增大后摘要更完整但增加上下文占用,减小后摘要更短。默认 400 字。' },
|
|
120
|
+
]
|
|
104
121
|
function TuneRow({ label, value, unit, onChange, onSave, onRestore, def, range, hint }) {
|
|
105
122
|
return h('div', { 'data-bl-row': '' },
|
|
106
123
|
h('span', { 'data-bl-name': '' }, label),
|
|
@@ -127,11 +144,17 @@ window.__ModuleLoader__.load({
|
|
|
127
144
|
const [ffTmo, setFfTmo] = React.useState('300')
|
|
128
145
|
const [cmdTmo, setCmdTmo] = React.useState('180')
|
|
129
146
|
const [advOpen, setAdvOpen] = React.useState(false)
|
|
147
|
+
// 高级设置那 8 项统一放在一个对象里,避免再加 8 个 useState
|
|
148
|
+
const [advVals, setAdvVals] = React.useState({})
|
|
130
149
|
const [tuneMeta, setTuneMeta] = React.useState(null)
|
|
131
150
|
const [qr, setQr] = React.useState(null)
|
|
132
151
|
const [qrState, setQrState] = React.useState('')
|
|
133
152
|
const [dlg, setDlg] = React.useState(null) // 'download' | 'clean'
|
|
134
153
|
const [picked, setPicked] = React.useState([])
|
|
154
|
+
// BLK-03(2026-10-28):8 秒轮询原来无条件把输入框重置成服务端值,用户改完天数还没点保存
|
|
155
|
+
// 就被改回 7。用 ref 记「用户动过输入框」,轮询见到就跳过覆盖,保存成功后复位。
|
|
156
|
+
const dirtyRef = React.useRef(false)
|
|
157
|
+
const markDirty = () => { dirtyRef.current = true }
|
|
135
158
|
|
|
136
159
|
const load = React.useCallback(async () => {
|
|
137
160
|
try {
|
|
@@ -139,19 +162,22 @@ window.__ModuleLoader__.load({
|
|
|
139
162
|
if (j.ok) {
|
|
140
163
|
setSt(j.data)
|
|
141
164
|
setErr('')
|
|
142
|
-
setDays(String(j.data.cache?.days ?? 7))
|
|
143
|
-
setMaxMb(String(j.data.cache?.maxMb ?? 2048))
|
|
144
165
|
// 参数分层:初值取 /status 的 tuning / tuningDefaults / tuningRanges
|
|
145
166
|
const tv = j.data.tuning || {}
|
|
146
167
|
const td = j.data.tuningDefaults || {}
|
|
147
168
|
const tr = j.data.tuningRanges || {}
|
|
148
169
|
setTuneMeta({ defaults: td, ranges: tr })
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
if (!dirtyRef.current) {
|
|
171
|
+
setDays(String(j.data.cache?.days ?? 7))
|
|
172
|
+
setMaxMb(String(j.data.cache?.maxMb ?? 2048))
|
|
173
|
+
const sec = Number(tv.asrMaxSec ?? td.asrMaxSec ?? 1800)
|
|
174
|
+
setAsrMin(String(Math.round(sec / 60)))
|
|
175
|
+
setFrames(String(tv.maxFrames ?? td.maxFrames ?? 500))
|
|
176
|
+
setAsrTmo(String(tv.asrTimeoutSec ?? td.asrTimeoutSec ?? 900))
|
|
177
|
+
setFfTmo(String(tv.ffmpegTimeoutSec ?? td.ffmpegTimeoutSec ?? 300))
|
|
178
|
+
setCmdTmo(String(tv.cmdTimeoutSec ?? td.cmdTimeoutSec ?? 180))
|
|
179
|
+
setAdvVals(Object.fromEntries(ADV_TUNING.map((x) => [x.key, String(tv[x.key] ?? td[x.key] ?? x.def)])))
|
|
180
|
+
}
|
|
155
181
|
} else {
|
|
156
182
|
setErr(j.why || '状态读取失败')
|
|
157
183
|
}
|
|
@@ -186,7 +212,7 @@ window.__ModuleLoader__.load({
|
|
|
186
212
|
if (j.state === 'expired' || j.state === 'error') {
|
|
187
213
|
stop = true
|
|
188
214
|
setQrState(j.state)
|
|
189
|
-
setMsg(j.why || (j.state === 'expired' ? '
|
|
215
|
+
setMsg(j.why || (j.state === 'expired' ? '二维码已过期,请点击「重新扫码」重试。' : '轮询失败'))
|
|
190
216
|
return
|
|
191
217
|
}
|
|
192
218
|
setQrState(j.state)
|
|
@@ -230,7 +256,7 @@ window.__ModuleLoader__.load({
|
|
|
230
256
|
setMsg('')
|
|
231
257
|
try {
|
|
232
258
|
const j = await jpost('/download', { target })
|
|
233
|
-
setMsg(j.ok ?
|
|
259
|
+
setMsg(j.ok ? `已开始下载(后台执行,日志 ${j.data.log})。下载完成后点击刷新查看状态。` : (j.data?.why || j.why || '启动失败'))
|
|
234
260
|
} catch (e) {
|
|
235
261
|
setMsg(String(e?.message || e))
|
|
236
262
|
} finally {
|
|
@@ -241,7 +267,8 @@ window.__ModuleLoader__.load({
|
|
|
241
267
|
const saveCache = async () => {
|
|
242
268
|
try {
|
|
243
269
|
const j = await jpost('/cache/clear', { days: Number(days), maxMb: Number(maxMb) })
|
|
244
|
-
setMsg(j.ok ?
|
|
270
|
+
setMsg(j.ok ? `缓存已清理(${j.data.removed} 个文件 / ${fmtBytes(j.data.bytes)});此后按 ${days} 天、${maxMb} MB 自动清理` : (j.why || '清理缓存失败'))
|
|
271
|
+
dirtyRef.current = false
|
|
245
272
|
await load()
|
|
246
273
|
} catch (e) {
|
|
247
274
|
setMsg(String(e?.message || e))
|
|
@@ -252,6 +279,7 @@ window.__ModuleLoader__.load({
|
|
|
252
279
|
try {
|
|
253
280
|
const j = await jpost('/config', { days: Number(days), maxMb: Number(maxMb) })
|
|
254
281
|
setMsg(j?.ok === false ? ('保存失败:' + (j.why || '未知错误')) : '自动清理规则已保存。')
|
|
282
|
+
if (j?.ok !== false) { dirtyRef.current = false; await load() }
|
|
255
283
|
} catch (e) {
|
|
256
284
|
// A2(2026-10-27 批次8):原来 catch{} 静默吞,保存失败用户完全无感——现在显式提示
|
|
257
285
|
setMsg('保存失败:' + String(e?.message || e))
|
|
@@ -267,6 +295,7 @@ window.__ModuleLoader__.load({
|
|
|
267
295
|
return
|
|
268
296
|
}
|
|
269
297
|
setMsg(okText)
|
|
298
|
+
dirtyRef.current = false
|
|
270
299
|
await load()
|
|
271
300
|
} catch (e) {
|
|
272
301
|
setMsg('保存失败:' + String(e?.message || e))
|
|
@@ -305,7 +334,7 @@ window.__ModuleLoader__.load({
|
|
|
305
334
|
await jpost('/download', { target: t })
|
|
306
335
|
} catch {}
|
|
307
336
|
}
|
|
308
|
-
setMsg(`已启动 ${picked.length}
|
|
337
|
+
setMsg(`已启动 ${picked.length} 个下载(后台执行,稍后点击刷新查看状态)。`)
|
|
309
338
|
return
|
|
310
339
|
}
|
|
311
340
|
if (!picked.length) {
|
|
@@ -325,7 +354,7 @@ window.__ModuleLoader__.load({
|
|
|
325
354
|
|
|
326
355
|
const delOne = async (target) => {
|
|
327
356
|
const row = rows.find((d) => d.id === target)
|
|
328
|
-
const label = target === 'credential' ? '
|
|
357
|
+
const label = target === 'credential' ? '登录凭证(删除后需重新扫码)' : (row?.name || target)
|
|
329
358
|
if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
|
|
330
359
|
if (!window.confirm(`确定删除 ${label}?`)) return
|
|
331
360
|
}
|
|
@@ -333,7 +362,7 @@ window.__ModuleLoader__.load({
|
|
|
333
362
|
const j = await jpost('/delete', { targets: [target] })
|
|
334
363
|
const d = j.data || {}
|
|
335
364
|
const skipped = (d.skipped || []).map((s) => s.why).join('、')
|
|
336
|
-
setMsg(d.deleted?.length ? `已删除 ${target}` :
|
|
365
|
+
setMsg(d.deleted?.length ? `已删除 ${target}` : `未删除:${skipped || '未知原因'}`)
|
|
337
366
|
await load()
|
|
338
367
|
} catch (e) {
|
|
339
368
|
setMsg('删除失败:' + String(e?.message || e))
|
|
@@ -380,7 +409,7 @@ window.__ModuleLoader__.load({
|
|
|
380
409
|
),
|
|
381
410
|
h('div', { 'data-bl-head': '' },
|
|
382
411
|
h('span', { 'data-bl-title': '' }, '依赖与状态'),
|
|
383
|
-
h('span', { 'data-bl-sub': '' }, '
|
|
412
|
+
h('span', { 'data-bl-sub': '' }, '缺失项点击行末按钮安装,完成后状态显示为已就绪。')),
|
|
384
413
|
h('div', { 'data-bl-sec': '' },
|
|
385
414
|
h('div', { 'data-bl-sechead': '' },
|
|
386
415
|
h('span', { 'data-bl-sec-title': '' }, '运行依赖'),
|
|
@@ -388,7 +417,7 @@ window.__ModuleLoader__.load({
|
|
|
388
417
|
h('button', { 'data-bl-btn': '', onClick: () => openDlg('clean') }, '批量清理')),
|
|
389
418
|
...deps.map((d) => h(DepRow, { key: d.id, d, busy: busy === d.id, onDownload: download, onDelete: delOne })),
|
|
390
419
|
h('div', { 'data-bl-note': '' },
|
|
391
|
-
'
|
|
420
|
+
'下载在后台执行,使用内置 fetch-deps 脚本;体积为预估值,实际以完成后为准。')),
|
|
392
421
|
dlg
|
|
393
422
|
? h('div', { 'data-bl-sec': '', 'data-bl-dlg': '' },
|
|
394
423
|
h('span', { 'data-bl-sec-title': '' }, dlg === 'download' ? '批量下载(默认全勾选,可取消)' : '批量清理(默认全勾选,可取消)'),
|
|
@@ -415,7 +444,7 @@ window.__ModuleLoader__.load({
|
|
|
415
444
|
d.removable
|
|
416
445
|
? d.ok
|
|
417
446
|
? h('span', { 'data-bl-good': '' }, '可清理')
|
|
418
|
-
: h('span', { 'data-bl-note': '' }, '
|
|
447
|
+
: h('span', { 'data-bl-note': '' }, '无可清理项')
|
|
419
448
|
: h('span', { 'data-bl-note': '' }, d.removableWhy || '不在插件目录内'),
|
|
420
449
|
),
|
|
421
450
|
)),
|
|
@@ -431,10 +460,10 @@ window.__ModuleLoader__.load({
|
|
|
431
460
|
h('div', { 'data-bl-qrside': '' },
|
|
432
461
|
h('div', { 'data-bl-note': '' },
|
|
433
462
|
qrState === 'scanned'
|
|
434
|
-
? '
|
|
463
|
+
? '请在 B 站手机客户端中确认登录。'
|
|
435
464
|
: qrState === 'loading'
|
|
436
465
|
? '正在生成二维码…'
|
|
437
|
-
: '
|
|
466
|
+
: '请使用 B 站手机 App 扫描二维码,确认后自动刷新登录状态。'),
|
|
438
467
|
h('button', { 'data-bl-btn': '', onClick: () => { setQr(null); setQrState('') } }, '取消'))))
|
|
439
468
|
: null,
|
|
440
469
|
h('div', { 'data-bl-sec': '' },
|
|
@@ -467,13 +496,13 @@ window.__ModuleLoader__.load({
|
|
|
467
496
|
h('span', { 'data-bl-name': '' }, '自动清理规则'),
|
|
468
497
|
h('span', { 'data-bl-detail': '' },
|
|
469
498
|
'保留 ',
|
|
470
|
-
h('input', { 'data-bl-input': '', value: days, onChange: (e) => setDays(e.target.value), inputMode: 'numeric' }),
|
|
499
|
+
h('input', { 'data-bl-input': '', value: days, onChange: (e) => { markDirty(); setDays(e.target.value) }, inputMode: 'numeric' }),
|
|
471
500
|
' 天;总量超过 ',
|
|
472
|
-
h('input', { 'data-bl-input': '', value: maxMb, onChange: (e) => setMaxMb(e.target.value), inputMode: 'numeric' }),
|
|
501
|
+
h('input', { 'data-bl-input': '', value: maxMb, onChange: (e) => { markDirty(); setMaxMb(e.target.value) }, inputMode: 'numeric' }),
|
|
473
502
|
' MB 时从最旧的删。'),
|
|
474
503
|
h('button', { 'data-bl-btn': '', onClick: saveRule }, '保存规则')),
|
|
475
504
|
h('div', { 'data-bl-note': '' },
|
|
476
|
-
'
|
|
505
|
+
'即使不手动清理,也会按上述规则自动执行(每次打开面板与插件启动时检查一次)。')),
|
|
477
506
|
// 参数分层(阶段B)常规组:不折叠。转写闸门按分钟显示,存的是秒。
|
|
478
507
|
h('div', { 'data-bl-sec': '' },
|
|
479
508
|
h('span', { 'data-bl-sec-title': '' }, '转写与抽帧'),
|
|
@@ -481,7 +510,7 @@ window.__ModuleLoader__.load({
|
|
|
481
510
|
label: '转写闸门',
|
|
482
511
|
value: asrMin,
|
|
483
512
|
unit: '分钟',
|
|
484
|
-
onChange: setAsrMin,
|
|
513
|
+
onChange: (v) => { markDirty(); setAsrMin(v) },
|
|
485
514
|
onSave: saveAsrMax,
|
|
486
515
|
onRestore: () => saveTune({ asrMaxSec: null }, '转写闸门已恢复默认(30 分钟)。'),
|
|
487
516
|
def: '30 分钟',
|
|
@@ -492,7 +521,7 @@ window.__ModuleLoader__.load({
|
|
|
492
521
|
label: '抽帧张数上限',
|
|
493
522
|
value: frames,
|
|
494
523
|
unit: '张',
|
|
495
|
-
onChange: setFrames,
|
|
524
|
+
onChange: (v) => { markDirty(); setFrames(v) },
|
|
496
525
|
onSave: () => saveTune({ maxFrames: Number(frames) }, '抽帧张数上限已保存。'),
|
|
497
526
|
onRestore: () => saveTune({ maxFrames: null }, '抽帧张数上限已恢复默认(500 张)。'),
|
|
498
527
|
def: '500 张',
|
|
@@ -501,16 +530,16 @@ window.__ModuleLoader__.load({
|
|
|
501
530
|
}),
|
|
502
531
|
h('div', { 'data-bl-row': '' },
|
|
503
532
|
h('button', { 'data-bl-btn': '', onClick: () => setAdvOpen((v) => !v) },
|
|
504
|
-
(advOpen ? '▾ ' : '▸ ') + '
|
|
533
|
+
(advOpen ? '▾ ' : '▸ ') + '高级设置(一般保持默认)'))),
|
|
505
534
|
// 高级设置:折叠,默认收起。改错会让长视频静默降级,所以放在折叠区里。
|
|
506
535
|
advOpen
|
|
507
536
|
? h('div', { 'data-bl-sec': '' },
|
|
508
|
-
h('span', { 'data-bl-sec-title': '' }, '
|
|
537
|
+
h('span', { 'data-bl-sec-title': '' }, '高级设置(一般保持默认)'),
|
|
509
538
|
h(TuneRow, {
|
|
510
539
|
label: 'ASR 超时',
|
|
511
540
|
value: asrTmo,
|
|
512
541
|
unit: '秒',
|
|
513
|
-
onChange: setAsrTmo,
|
|
542
|
+
onChange: (v) => { markDirty(); setAsrTmo(v) },
|
|
514
543
|
onSave: () => saveTune({ asrTimeoutSec: Number(asrTmo) }, 'ASR 超时已保存。'),
|
|
515
544
|
onRestore: () => saveTune({ asrTimeoutSec: null }, 'ASR 超时已恢复默认(900 秒)。'),
|
|
516
545
|
def: '900 秒',
|
|
@@ -521,7 +550,7 @@ window.__ModuleLoader__.load({
|
|
|
521
550
|
label: 'ffmpeg 超时',
|
|
522
551
|
value: ffTmo,
|
|
523
552
|
unit: '秒',
|
|
524
|
-
onChange: setFfTmo,
|
|
553
|
+
onChange: (v) => { markDirty(); setFfTmo(v) },
|
|
525
554
|
onSave: () => saveTune({ ffmpegTimeoutSec: Number(ffTmo) }, 'ffmpeg 超时已保存。'),
|
|
526
555
|
onRestore: () => saveTune({ ffmpegTimeoutSec: null }, 'ffmpeg 超时已恢复默认(300 秒)。'),
|
|
527
556
|
def: '300 秒',
|
|
@@ -532,13 +561,29 @@ window.__ModuleLoader__.load({
|
|
|
532
561
|
label: '命令超时',
|
|
533
562
|
value: cmdTmo,
|
|
534
563
|
unit: '秒',
|
|
535
|
-
onChange: setCmdTmo,
|
|
564
|
+
onChange: (v) => { markDirty(); setCmdTmo(v) },
|
|
536
565
|
onSave: () => saveTune({ cmdTimeoutSec: Number(cmdTmo) }, '命令超时已保存。'),
|
|
537
566
|
onRestore: () => saveTune({ cmdTimeoutSec: null }, '命令超时已恢复默认(180 秒)。'),
|
|
538
567
|
def: '180 秒',
|
|
539
568
|
range: `${tuneMeta?.ranges?.cmdTimeoutSec?.[0] ?? 60}–${tuneMeta?.ranges?.cmdTimeoutSec?.[1] ?? 900} 秒`,
|
|
540
569
|
hint: '太短会让长视频的 text 命令超时。',
|
|
541
570
|
}),
|
|
571
|
+
// 2026-10-28 新增的 8 项:全部由 ADV_TUNING 驱动,加项只改那张表 + 后端 TUNING_DEFS
|
|
572
|
+
...ADV_TUNING.map((x) => h(TuneRow, {
|
|
573
|
+
key: x.key,
|
|
574
|
+
label: x.label,
|
|
575
|
+
value: advVals[x.key] ?? x.def,
|
|
576
|
+
unit: x.unit,
|
|
577
|
+
onChange: (v) => { markDirty(); setAdvVals((s) => ({ ...s, [x.key]: v })) },
|
|
578
|
+
onSave: () => saveTune({ [x.key]: Number(advVals[x.key] ?? x.def) }, `${x.label}已保存。`),
|
|
579
|
+
onRestore: () => saveTune({ [x.key]: null }, `${x.label}已恢复默认(${x.def} ${x.unit})。`),
|
|
580
|
+
def: `${x.def} ${x.unit}`,
|
|
581
|
+
range: (() => {
|
|
582
|
+
const r = tuneMeta?.ranges?.[x.key]
|
|
583
|
+
return r ? `${r[0]}–${r[1]} ${x.unit}` : `默认 ${x.def} ${x.unit}`
|
|
584
|
+
})(),
|
|
585
|
+
hint: x.hint,
|
|
586
|
+
})),
|
|
542
587
|
h('div', { 'data-bl-note': '' },
|
|
543
588
|
'这几项会写进 deps/deps-config.json,改完立即生效,不用重启。「恢复默认」= 把这个键从配置里删掉。'))
|
|
544
589
|
: null,
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# dsh-bili-watch bundle patch layer
|
|
2
2
|
# profile 的 dsh.profile.bundles 列出本包名即整层生效
|
|
3
|
+
# 2026-10-28:去掉 disabled: true。0.2.17(10-24 应急修 DNS rebinding)时误加,
|
|
4
|
+
# 之后 7 个版本一直带着,装完重启命令不存在、用户以为装坏了。不要再改回 true。
|
|
3
5
|
- insert:
|
|
4
6
|
- id: dsh-bili-watch
|
|
5
7
|
name: dsh-bili-watch
|
|
6
|
-
disabled: true
|
package/deps/_bili-asr.py
CHANGED
|
@@ -90,7 +90,9 @@ def main() -> int:
|
|
|
90
90
|
segments, info = model.transcribe(
|
|
91
91
|
args.src,
|
|
92
92
|
language=None if args.lang == "auto" else args.lang,
|
|
93
|
-
|
|
93
|
+
# issue #2 P3-1(2026-10-28):原来硬编码 True。VAD 预检失败(vad_ok=False)时,
|
|
94
|
+
# 同一个 vad 模块会在 transcribe 里再炸一次、整段转写直接失败;所以预检失败就关掉过滤。
|
|
95
|
+
vad_filter=vad_ok, # 正常情况掐掉静音段,纯画面/纯音乐的视频会得到空文本
|
|
94
96
|
vad_parameters={"min_silence_duration_ms": 400},
|
|
95
97
|
beam_size=5,
|
|
96
98
|
condition_on_previous_text=False, # 防止长视频里一段错就带着后面全错
|
package/index.js
CHANGED
|
@@ -18,8 +18,10 @@ import { fileURLToPath } from 'node:url'
|
|
|
18
18
|
// 缺了只少一个 bili_watch 工具,/bili 命令、插件面板与路由照常工作,不能因此整块加载失败。
|
|
19
19
|
// 工具输出上限:超了就把全文落文件、只回摘要。
|
|
20
20
|
// 由来:2026-10-26 一段 14 分钟视频的字幕 4,449 字整段塞进上下文,把会话挤爆两次。
|
|
21
|
-
|
|
22
|
-
const
|
|
21
|
+
// 2026-10-28 高级设置放开:两个上限改从 readTuning() 每次取(toolTextCap 默认 2000、toolHead 默认 800)。
|
|
22
|
+
// 不能写成模块级 const——index.js 是常驻进程,那样面板改了要重启才生效,和 cacheCfg 踩过的坑一样。
|
|
23
|
+
const toolTextCap = () => readTuning().toolTextCap
|
|
24
|
+
const toolTextHead = () => readTuning().toolHead
|
|
23
25
|
|
|
24
26
|
// ── 进程级路由守卫(T41-P0-1):fiber 重放/多实例下同一路径只注册一次 ──
|
|
25
27
|
// 参照 dsh-memory-evolve/lib/route-guard.js(MIT):claim 成功才注册;
|
|
@@ -80,10 +82,10 @@ function trimForModel(text, tag = 'out', bv = '', jsonMode = false, opts = {}) {
|
|
|
80
82
|
// A8(2026-10-27 批次8):--full 时工具层不要再把已放大的文本切回 800——
|
|
81
83
|
// full 走 20000 上限;用户给的 --head N 优先于默认 800;不传 full 仍 ≤2000。
|
|
82
84
|
const full = !!opts.full
|
|
83
|
-
const cap = full ? 20000 :
|
|
85
|
+
const cap = full ? 20000 : toolTextCap()
|
|
84
86
|
const head = (Number.isFinite(opts.head) && opts.head > 0)
|
|
85
87
|
? Math.floor(opts.head)
|
|
86
|
-
: (full ? 20000 :
|
|
88
|
+
: (full ? 20000 : toolTextHead())
|
|
87
89
|
let f = ''
|
|
88
90
|
try {
|
|
89
91
|
const base = String(tag || 'out').replace(/[^\w.-]/g, '') || 'out'
|
|
@@ -140,6 +142,9 @@ const DEPS = join(HERE, 'deps')
|
|
|
140
142
|
// 解析失败不写 stdout/stderr(会污染 --format json 与 PowerShell 管道),
|
|
141
143
|
// 而是记进 cfgWarning,由 /status 的 warnings 带给面板;ENOENT(没配过)不算异常。
|
|
142
144
|
let cfgWarning = null
|
|
145
|
+
// issue #2 P3-3(2026-10-28):bili_watch 工具没注册成功时原来只 console.warn,桌面端看不到控制台,
|
|
146
|
+
// 用户会以为「工具坏了」。改成同时记一条,由 /status 的 warnings 带到面板。
|
|
147
|
+
let toolsWarning = null
|
|
143
148
|
function depsCfg() {
|
|
144
149
|
try { return JSON.parse(readFileSync(join(DEPS, 'deps-config.json'), 'utf8')) } catch (e) {
|
|
145
150
|
if (e?.code !== 'ENOENT') {
|
|
@@ -159,6 +164,23 @@ const TUNING_DEFS = {
|
|
|
159
164
|
asrTimeoutSec: { def: 900, min: 300, max: 3600 },
|
|
160
165
|
ffmpegTimeoutSec: { def: 300, min: 60, max: 1800 },
|
|
161
166
|
cmdTimeoutSec: { def: 180, min: 60, max: 900 },
|
|
167
|
+
// 2026-10-28 老大要求「高级设置放开,能调的都给调」:下面 8 项原来是硬编码。
|
|
168
|
+
// 全部整数(tuneOne 会 Math.floor,别写小数);键集必须与 tools/look.mjs:2878 那份完全一致,
|
|
169
|
+
// verify.mjs 的 C17 会逐键比对,改一处漏一处直接红。
|
|
170
|
+
asrMemMB: { def: 1536, min: 512, max: 16384 }, // 跑本地转写要多少可用内存(MB),不够就跳
|
|
171
|
+
lockTtlMin: { def: 30, min: 5, max: 240 }, // 残留锁多久算过期(分钟)
|
|
172
|
+
unitLimit: { def: 5000, min: 1000, max: 20000 }, // 单个内容单元(字幕段/评论)字符上限
|
|
173
|
+
textLimit: { def: 800, min: 200, max: 5000 }, // 文本层默认给多少字
|
|
174
|
+
toolTextCap: { def: 2000, min: 500, max: 12000 },// 工具层单次回给模型的总字数(调大会挤占对话上下文)
|
|
175
|
+
toolHead: { def: 800, min: 200, max: 5000 }, // 工具层默认 head 字数
|
|
176
|
+
dmBucketSec: { def: 30, min: 5, max: 120 }, // 弹幕热区按多少秒一桶
|
|
177
|
+
dmTop: { def: 5, min: 1, max: 20 }, // 弹幕热区取前几桶
|
|
178
|
+
// 2026-10-28 第二批 3 项:都是「撞上了只能报 bug、自己没法自救」的。
|
|
179
|
+
// 语速用 ×10 整数存(20 = 2.0 字/秒),因为 tuneOne 会 Math.floor,不能写小数。
|
|
180
|
+
downloadMaxMB: { def: 512, min: 64, max: 4096 }, // 单个视频/音频下载上限(4K 素材会撞)
|
|
181
|
+
subRateMin10: { def: 20, min: 5, max: 60 }, // 字幕语速下限×10,低于判「可疑」
|
|
182
|
+
subRateMax10: { def: 85, min: 30, max: 200 }, // 字幕语速上限×10,高于判「可疑」
|
|
183
|
+
textBrief: { def: 400, min: 100, max: 3000 }, // 超长文本只给开头多少字(其余落盘)
|
|
162
184
|
}
|
|
163
185
|
const TUNING_KEYS = Object.keys(TUNING_DEFS)
|
|
164
186
|
const tuningDefaults = () => Object.fromEntries(TUNING_KEYS.map((k) => [k, TUNING_DEFS[k].def]))
|
|
@@ -201,7 +223,7 @@ function writeTuning(body) {
|
|
|
201
223
|
writeFileSync(p, JSON.stringify(cur, null, 2), 'utf8')
|
|
202
224
|
return { ok: true, data: cur }
|
|
203
225
|
} catch (e) {
|
|
204
|
-
return { ok: false,
|
|
226
|
+
return { ok: false, why: String(e?.message || e) }
|
|
205
227
|
}
|
|
206
228
|
}
|
|
207
229
|
function projDir() { return process.env.BILI_WATCH_PROJ || depsCfg().proj || DEPS }
|
|
@@ -213,7 +235,23 @@ function projDir() { return process.env.BILI_WATCH_PROJ || depsCfg().proj || DEP
|
|
|
213
235
|
const SHARED_CRED = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'bili-watch', 'credential.json')
|
|
214
236
|
const PANEL_CRED = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'bili-duty', 'bili_credential.json')
|
|
215
237
|
const LEGACY_CRED = join(DEPS, 'credential.json')
|
|
238
|
+
// 2026-10-28:面板扫码写的是 bili-duty\bili_credential.small.json(小号),watch 这份不会跟着变。
|
|
239
|
+
// 老大的口径是「两个地方用同一份,复制一下即可」——所以这里把它做成一次自动复制,
|
|
240
|
+
// 不引入新的读取优先级:复制后 SHARED 的 mtime 就是现在,后面那段 mtime 仲裁照旧。
|
|
241
|
+
// 单向(panel small → SHARED),不反向写,避免把 watch 扫的号盖掉面板那份。
|
|
242
|
+
const PANEL_SMALL = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'bili-duty', 'bili_credential.small.json')
|
|
243
|
+
function syncPanelSmallToShared() {
|
|
244
|
+
try {
|
|
245
|
+
const panelAt = statSync(PANEL_SMALL).mtimeMs
|
|
246
|
+
let sharedAt = 0
|
|
247
|
+
try { sharedAt = statSync(SHARED_CRED).mtimeMs } catch {}
|
|
248
|
+
if (sharedAt && panelAt <= sharedAt) return
|
|
249
|
+
mkdirSync(dirname(SHARED_CRED), { recursive: true })
|
|
250
|
+
writeFileSync(SHARED_CRED, readFileSync(PANEL_SMALL), { encoding: 'utf8', mode: 0o600 })
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
216
253
|
function resolveCredPath() {
|
|
254
|
+
syncPanelSmallToShared()
|
|
217
255
|
try {
|
|
218
256
|
// mtime 比较:panel 侧重登大号(更常见)后 PANEL_CRED 更新,watch 立刻跟上;
|
|
219
257
|
// watch 自己扫码登了新号则 SHARED 更新,用 SHARED。同 mtime 时偏好 panel(权威源)。
|
|
@@ -244,23 +282,23 @@ const LAST_RUN = join(DEPS, 'last-run.json')
|
|
|
244
282
|
// 自定义目录不可写时不让功能哑掉:回退到默认路径($DSH_HOME/bili-watch/cache),并把原因挂进 /status.warnings。
|
|
245
283
|
// 缓存根标记:只有我们自己写过这个文件,才认「这个目录已经是缓存根」
|
|
246
284
|
const CACHE_MARK = '.bili-watch-root'
|
|
247
|
-
const OWN_CACHE_DIRS = new Set(['_logs', 'frames', 'sheets', 'sheet-tmp', 'substrip-tmp', '_misc', 'asr', 'frames-dl', 'audio-dl'])
|
|
285
|
+
const OWN_CACHE_DIRS = new Set(['_logs', 'frames', 'sheets', 'sheet-tmp', 'substrip-tmp', '_misc', 'asr', 'frames-dl', 'audio-dl', '_hot'])
|
|
248
286
|
function hasBvLayout(dir) {
|
|
249
287
|
// 判据一:我们写过的标记(新用户、二次运行都靠它)
|
|
250
288
|
try { if (existsSync(join(dir, CACHE_MARK))) return true } catch {}
|
|
251
|
-
// 判据二(
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
289
|
+
// 判据二(2026-10-28 W-01 改):BV* 目录 **且** 该目录里有我们自己写的 meta.json。
|
|
290
|
+
// N05-A1 那版用「BV* + frames/asr/_logs 之一」,实测仍会误判:用户素材库只要同时有 BV 目录和
|
|
291
|
+
// 一个 frames 目录,clean 就把 BV 目录整片删掉(rc=0、无警告),还顺手写下 .bili-watch-root 自我固化,
|
|
292
|
+
// 此后永远走删的那条路、不会自己纠正。frames/asr/_logs 是通用名,等于拿别人的东西证明自己的身份;
|
|
293
|
+
// meta.json 只有 writeMeta() 会写,用户素材库不会有。
|
|
255
294
|
// 口径必须与 tools/look.mjs 的同名函数逐字一致,否则面板与 CLI 判据分叉。
|
|
256
|
-
let hasBv = false
|
|
257
|
-
let hasWork = false
|
|
258
295
|
try {
|
|
259
296
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
260
297
|
if (!e.isDirectory()) continue
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
298
|
+
if (!/^BV[0-9A-Za-z]{10}/i.test(e.name)) continue
|
|
299
|
+
try {
|
|
300
|
+
if (existsSync(join(dir, e.name, 'meta.json'))) return true
|
|
301
|
+
} catch {}
|
|
264
302
|
}
|
|
265
303
|
} catch {}
|
|
266
304
|
return false
|
|
@@ -281,7 +319,8 @@ function isOwnedEntry(p) {
|
|
|
281
319
|
// 留下一个 .lock 会让 autoCleanCache 永远 locked、而唯一逃生口 clean 用同一判据也被堵死,
|
|
282
320
|
// 缓存只涨不消且没有 --force。这里与 tryLock 用完全相同的判据:持有者进程还活着且未过期才算被占用。
|
|
283
321
|
// 下面三个辅助函数与 tools/look.mjs:647-662 逐字一致。
|
|
284
|
-
|
|
322
|
+
// 2026-10-28 高级设置放开:与 tools/look.mjs 同源,从 readTuning().lockTtlMin 取(默认 30 分钟)
|
|
323
|
+
const lockTtlMs = () => readTuning().lockTtlMin * 60 * 1000
|
|
285
324
|
function pidAlive(pid) {
|
|
286
325
|
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
287
326
|
try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' }
|
|
@@ -295,7 +334,7 @@ function readLock(f) {
|
|
|
295
334
|
}
|
|
296
335
|
function lockExpired(owner) {
|
|
297
336
|
if (!owner) return true
|
|
298
|
-
return Date.now() - (owner.at || 0) > (owner.ttlMs ||
|
|
337
|
+
return Date.now() - (owner.at || 0) > (owner.ttlMs || lockTtlMs())
|
|
299
338
|
}
|
|
300
339
|
function hasAnyLock(root) {
|
|
301
340
|
const walk = (d, depth) => {
|
|
@@ -316,15 +355,23 @@ function hasAnyLock(root) {
|
|
|
316
355
|
}
|
|
317
356
|
// 2026-10-28:cleanKey 会把整条链接压成目录名(如 httpsb23.tvabc123)——短链没解析成 BV 时
|
|
318
357
|
// 插件自己就建了这种目录,清理时必须认它,否则「立即清除」会留一堆删不掉的垃圾(用户实测剩 3 个)。
|
|
319
|
-
//
|
|
358
|
+
// 10-28 又实测剩两个:`search` 的落盘目录名来自查询词(如 `DeepSeekHarness`)、`hot` 落 `_hot`。
|
|
359
|
+
// 判据:里面**只有**我们自己的产物,且至少有一个 `*.tool.txt`。用户素材目录装的是 mp4/jpg,不会是这个形态;
|
|
360
|
+
// 要求「至少一个 tool.txt」是给「名字不像 URL 的目录」加门槛,空目录仍只认名字像 URL 的。
|
|
320
361
|
function looksLikeOwnUrlDir(p, name) {
|
|
321
|
-
if (!/^https?/i.test(name)) return false
|
|
322
362
|
let ents
|
|
323
363
|
try { ents = readdirSync(p, { withFileTypes: true }) } catch { return false }
|
|
324
|
-
if (!ents.length) return
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
364
|
+
if (!ents.length) return /^https?/i.test(name)
|
|
365
|
+
let hasToolTxt = false
|
|
366
|
+
for (const e of ents) {
|
|
367
|
+
if (e.isDirectory()) {
|
|
368
|
+
if (!OWN_CACHE_DIRS.has(e.name)) return false
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
if (/\.tool\.txt$/i.test(e.name)) hasToolTxt = true
|
|
372
|
+
else if (!/^(meta\.json|.*\.lock)$/i.test(e.name)) return false
|
|
373
|
+
}
|
|
374
|
+
return hasToolTxt || /^https?/i.test(name)
|
|
328
375
|
}
|
|
329
376
|
function removeOwned(dir) {
|
|
330
377
|
let files = 0
|
|
@@ -753,6 +800,9 @@ function dirStat(dir) {
|
|
|
753
800
|
let files = 0
|
|
754
801
|
const walk = (d) => {
|
|
755
802
|
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
803
|
+
// 10-28:标记文件与运行日志不算「缓存占用」——面板「当前占用」应当等于「立即清除能清掉的量」,
|
|
804
|
+
// 否则清了以后永远显示 52 B + 日志大小,用户以为没清干净。
|
|
805
|
+
if (e.name === CACHE_MARK || e.name === '_logs') continue
|
|
756
806
|
const p = join(d, e.name)
|
|
757
807
|
if (e.isDirectory()) walk(p)
|
|
758
808
|
else {
|
|
@@ -826,9 +876,12 @@ function writeDepsCfg(patch) {
|
|
|
826
876
|
const next = { ...cur, ...patch }
|
|
827
877
|
try {
|
|
828
878
|
writeFileSync(p, JSON.stringify(next, null, 2), 'utf8')
|
|
879
|
+
// issue #2 P2-2(2026-10-28):resolveFfmpeg 有 5 分钟缓存,改了 ffmpeg 路径后 /status 还会
|
|
880
|
+
// 报旧路径 5 分钟。配置一写就清缓存,下一次 /status 立刻重探。
|
|
881
|
+
ffmpegProbeCache = { at: 0, path: '' }
|
|
829
882
|
return { ok: true, data: next }
|
|
830
883
|
} catch (e) {
|
|
831
|
-
return { ok: false,
|
|
884
|
+
return { ok: false, why: String(e?.message || e) }
|
|
832
885
|
}
|
|
833
886
|
}
|
|
834
887
|
|
|
@@ -1009,7 +1062,13 @@ function depRows() {
|
|
|
1009
1062
|
id: 'ytdlp',
|
|
1010
1063
|
name: 'yt-dlp(通用 URL)',
|
|
1011
1064
|
ok: existsSync(ytdlp),
|
|
1012
|
-
|
|
1065
|
+
// 2026-10-28:原先 detail 只写「版本 X」,与其它依赖行(detail 都是路径)不一致,
|
|
1066
|
+
// 面板上就变成「只有版本号、看不出装在哪」。改成路径,版本拼在括号里。
|
|
1067
|
+
detail: (() => {
|
|
1068
|
+
if (!existsSync(ytdlp)) return '没找到,非 B 站 URL 不可用'
|
|
1069
|
+
const v = ytdlpVersion()
|
|
1070
|
+
return `${maskAbs(ytdlp)}${v ? `(v${v})` : ''}`
|
|
1071
|
+
})(),
|
|
1013
1072
|
path: maskAbs(ytdlp),
|
|
1014
1073
|
removable: true,
|
|
1015
1074
|
size: ytdlpSize,
|
|
@@ -1087,7 +1146,7 @@ function runDownload(target) {
|
|
|
1087
1146
|
try {
|
|
1088
1147
|
child = spawn(cmd, args, { windowsHide: true, stdio: 'ignore', detached: true })
|
|
1089
1148
|
} catch (e) {
|
|
1090
|
-
return { started: false,
|
|
1149
|
+
return { started: false, why: String(e?.message ?? e) }
|
|
1091
1150
|
}
|
|
1092
1151
|
// detached 子进程不会把 spawn 失败同步抛给我们,必须自己接 error,否则 ENOENT 会带崩宿主
|
|
1093
1152
|
child.on('error', (e) => {
|
|
@@ -1187,9 +1246,18 @@ function sameOriginBlocked(req) {
|
|
|
1187
1246
|
const method = String(req.method || 'GET').toUpperCase()
|
|
1188
1247
|
const mutating = method !== 'GET' && method !== 'HEAD'
|
|
1189
1248
|
const origin = req.headers?.origin
|
|
1249
|
+
const site = req.headers?.["sec-fetch-site"]
|
|
1190
1250
|
// 14-6:写操作必须带 Origin 且同源。不带 Origin 的(curl 直打、旧浏览器)一律拒——
|
|
1191
1251
|
// 原本 `if (origin)` 是可选的,等于裸 POST 没有任何同源校验。
|
|
1192
|
-
|
|
1252
|
+
// 2026-10-28 issue #2 P1-1 修正:DSH 桌面端壳层的同源 fetch **不带 Origin**(实测复现:
|
|
1253
|
+
// 无 Origin 的 POST → 403「缺少 Origin 头」,面板所有写操作全废)。改成有 Origin 才严格同源,
|
|
1254
|
+
// 没 Origin 时只拒 sec-fetch-site 明确写着跨站的(cross-site / same-site),
|
|
1255
|
+
// 其余交给下面「必须 application/json」那条兜——CSRF 的简单请求向量走不到这里。
|
|
1256
|
+
if (mutating && !origin) {
|
|
1257
|
+
if (site === "cross-site" || site === "same-site") {
|
|
1258
|
+
return `跨站写请求被拒绝(缺少 Origin 且 sec-fetch-site=${site})`
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1193
1261
|
if (origin) {
|
|
1194
1262
|
let oh = ""
|
|
1195
1263
|
try { oh = new URL(origin).hostname } catch { return "非法 Origin 头" }
|
|
@@ -1201,7 +1269,7 @@ function sameOriginBlocked(req) {
|
|
|
1201
1269
|
const ct = String(req.headers?.['content-type'] || '').split(';')[0].trim().toLowerCase()
|
|
1202
1270
|
if (ct !== 'application/json') return `跨站写请求被拒绝(Content-Type 必须是 application/json,收到:${ct || '无'})`
|
|
1203
1271
|
}
|
|
1204
|
-
|
|
1272
|
+
// site 在上面写操作分支已取过(2026-10-28 issue #2 P1-1),这里直接复用,不要重复声明。
|
|
1205
1273
|
if (site && site !== "same-origin" && site !== "none") return `跨站请求被拒绝(sec-fetch-site=${site})`
|
|
1206
1274
|
return null
|
|
1207
1275
|
}
|
|
@@ -1590,7 +1658,8 @@ function apply(ctx) {
|
|
|
1590
1658
|
},
|
|
1591
1659
|
}))
|
|
1592
1660
|
} else {
|
|
1593
|
-
|
|
1661
|
+
toolsWarning = '[dsh-bili-watch] 未找到 @deepseek-ai/dsh-tools,bili_watch 工具未注册(/bili 命令与插件面板不受影响)'
|
|
1662
|
+
console.warn(toolsWarning)
|
|
1594
1663
|
}
|
|
1595
1664
|
|
|
1596
1665
|
// ③ 设置页:读盘状态 + 清缓存(只读优先;唯一写操作是删本机抽帧缓存目录)
|
|
@@ -1604,7 +1673,7 @@ function apply(ctx) {
|
|
|
1604
1673
|
path: '/api/bili-watch',
|
|
1605
1674
|
handler: async (req, res) => {
|
|
1606
1675
|
const blocked = sameOriginBlocked(req)
|
|
1607
|
-
if (blocked) return sendJson(res, 403, { ok: false,
|
|
1676
|
+
if (blocked) return sendJson(res, 403, { ok: false, why: blocked })
|
|
1608
1677
|
try {
|
|
1609
1678
|
const url = new URL(req.url, 'http://127.0.0.1')
|
|
1610
1679
|
if (req.method === 'GET' && url.pathname === '/api/bili-watch/status') {
|
|
@@ -1620,7 +1689,7 @@ function apply(ctx) {
|
|
|
1620
1689
|
tuningRanges: tuningRanges(),
|
|
1621
1690
|
lastRun: readLastRun(),
|
|
1622
1691
|
ui: uiCfg(),
|
|
1623
|
-
warnings:
|
|
1692
|
+
warnings: [cfgWarning, toolsWarning].filter(Boolean),
|
|
1624
1693
|
},
|
|
1625
1694
|
})
|
|
1626
1695
|
}
|
|
@@ -1639,7 +1708,7 @@ function apply(ctx) {
|
|
|
1639
1708
|
// 6-17:写 deps-config.json 可能失败(文件只读等),原来直接抛错被外层吞成 500,
|
|
1640
1709
|
// 面板可能显示已保存;这里显式回「保存失败」。
|
|
1641
1710
|
const w = writeDepsCfg({ cache: dir })
|
|
1642
|
-
if (!w.ok) return sendJson(res, 500, { ok: false, why: `保存失败:${w.
|
|
1711
|
+
if (!w.ok) return sendJson(res, 500, { ok: false, why: `保存失败:${w.why || '未知错误'}` })
|
|
1643
1712
|
return sendJson(res, 200, { ok: true, data: { dir, restart: true } })
|
|
1644
1713
|
} catch (e) {
|
|
1645
1714
|
return sendJson(res, 500, { ok: false, why: String(e?.message || e) })
|
|
@@ -1727,7 +1796,7 @@ function apply(ctx) {
|
|
|
1727
1796
|
let tuneErr = null
|
|
1728
1797
|
if (TUNING_KEYS.some((k) => body && Object.prototype.hasOwnProperty.call(body, k))) {
|
|
1729
1798
|
const w = writeTuning(body)
|
|
1730
|
-
if (!w.ok) tuneErr = w.
|
|
1799
|
+
if (!w.ok) tuneErr = w.why || '未知错误'
|
|
1731
1800
|
}
|
|
1732
1801
|
if (tuneErr) return sendJson(res, 500, { ok: false, why: `参数保存失败:${tuneErr}` })
|
|
1733
1802
|
return sendJson(res, 200, { ok: true, data: { ...next, ...(rule || {}), tuning: readTuning() } })
|
package/package.json
CHANGED
package/scripts/fetch-deps.ps1
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
· -DryRun 只打印将执行的命令,不下载不写盘。
|
|
22
22
|
· 国内网络用 -Mirror cn:GitHub 走 gh-proxy、PyPI 走清华、模型走 hf-mirror。
|
|
23
23
|
· ffmpeg 来源是 BtbN 的 latest 构建,版本随上游变化,不锁具体版本。
|
|
24
|
-
· pip
|
|
24
|
+
· pip 包锁死四版本(本机实测能跑):faster-whisper==1.2.1、av==15.1.0、
|
|
25
|
+
ctranslate2==4.8.3(10-28 issue #2 P3-2:4.8.4 在部分机器加载模型即 0xC0000005 硬崩,
|
|
26
|
+
自己没有显式依赖,是 faster-whisper 拖进来的,所以必须自己钉)、
|
|
25
27
|
rapidocr-onnxruntime==1.2.3。yt-dlp 不锁版本,必须跟上游走。
|
|
26
28
|
· 二进制不锁版本,但装完会把 ffmpeg -version 首行与 yt-dlp --version 记进
|
|
27
29
|
deps\last-run.json(便于对账;该文件同时是运行时记录,下次跑 CLI 会被覆盖)。
|
|
@@ -137,14 +139,14 @@ function Ensure-Asr {
|
|
|
137
139
|
Write-Step 'ASR 环境(deps\.venv-asr + faster-whisper==1.2.1)'
|
|
138
140
|
if ($DryRun) {
|
|
139
141
|
Write-Host " [dry-run] <系统 Python> -m venv `"$venv`""
|
|
140
|
-
Write-Host " [dry-run] `"$py`" -m pip install `"faster-whisper==1.2.1`" `"av==15.1.0`" -i $PyIndex"
|
|
142
|
+
Write-Host " [dry-run] `"$py`" -m pip install `"faster-whisper==1.2.1`" `"av==15.1.0`" `"ctranslate2==4.8.3`" -i $PyIndex"
|
|
141
143
|
return
|
|
142
144
|
}
|
|
143
145
|
$sysPy = Get-SystemPython
|
|
144
146
|
& $sysPy -m venv $venv
|
|
145
147
|
if ($LASTEXITCODE -ne 0) { throw '创建 venv 失败' }
|
|
146
148
|
& $py -m pip install --upgrade pip -i $PyIndex
|
|
147
|
-
& $py -m pip install "faster-whisper==1.2.1" "av==15.1.0" -i $PyIndex
|
|
149
|
+
& $py -m pip install "faster-whisper==1.2.1" "av==15.1.0" "ctranslate2==4.8.3" -i $PyIndex
|
|
148
150
|
if ($LASTEXITCODE -ne 0) {
|
|
149
151
|
# 本次失败立刻清掉半成品,别留给下次 Skip 继续踩
|
|
150
152
|
Remove-Item -LiteralPath $venv -Recurse -Force -ErrorAction SilentlyContinue
|