dsh-boot-suite 2.13.11 → 2.13.19
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/CHANGELOG.md +87 -0
- package/README.md +3 -1
- package/lib/assets/voices/f/line-1.mp3 +0 -0
- package/lib/assets/voices/f/line-2.mp3 +0 -0
- package/lib/assets/voices/f/line-3.mp3 +0 -0
- package/lib/assets/voices/f/line-4.mp3 +0 -0
- package/lib/assets/voices/f/line-5.mp3 +0 -0
- package/lib/assets/voices/f/line-6.mp3 +0 -0
- package/lib/assets/voices/g/line-1.mp3 +0 -0
- package/lib/assets/voices/g/line-2.mp3 +0 -0
- package/lib/assets/voices/g/line-3.mp3 +0 -0
- package/lib/assets/voices/g/line-4.mp3 +0 -0
- package/lib/assets/voices/g/line-5.mp3 +0 -0
- package/lib/assets/voices/g/line-6.mp3 +0 -0
- package/lib/assets/voices/h/line-1.mp3 +0 -0
- package/lib/assets/voices/h/line-2.mp3 +0 -0
- package/lib/assets/voices/h/line-3.mp3 +0 -0
- package/lib/assets/voices/h/line-4.mp3 +0 -0
- package/lib/assets/voices/h/line-5.mp3 +0 -0
- package/lib/assets/voices/h/line-6.mp3 +0 -0
- package/lib/assets/voices/i/line-1.mp3 +0 -0
- package/lib/assets/voices/i/line-2.mp3 +0 -0
- package/lib/assets/voices/i/line-3.mp3 +0 -0
- package/lib/assets/voices/i/line-4.mp3 +0 -0
- package/lib/assets/voices/i/line-5.mp3 +0 -0
- package/lib/assets/voices/i/line-6.mp3 +0 -0
- package/lib/assets/voices/j/line-1.mp3 +0 -0
- package/lib/assets/voices/j/line-2.mp3 +0 -0
- package/lib/assets/voices/j/line-3.mp3 +0 -0
- package/lib/assets/voices/j/line-4.mp3 +0 -0
- package/lib/assets/voices/j/line-5.mp3 +0 -0
- package/lib/assets/voices/j/line-6.mp3 +0 -0
- package/lib/assets/voices/k/line-1.mp3 +0 -0
- package/lib/assets/voices/k/line-2.mp3 +0 -0
- package/lib/assets/voices/k/line-3.mp3 +0 -0
- package/lib/assets/voices/k/line-4.mp3 +0 -0
- package/lib/assets/voices/k/line-5.mp3 +0 -0
- package/lib/assets/voices/k/line-6.mp3 +0 -0
- package/lib/assets/voices/manifest.json +32 -0
- package/lib/client.js +145 -64
- package/lib/host-startup.js +81 -4
- package/lib/host-wallpaper.js +8 -2
- package/lib/splash.js +73 -23
- package/package.json +2 -2
- package/test/smoke-client.mjs +27 -4
- package/test/smoke-host.mjs +29 -4
- package/test/smoke-package.mjs +4 -1
- package/test/smoke-splash.mjs +8 -1
- package/test/smoke-wallpaper-client.mjs +1 -1
- package/test/smoke-wallpaper-host.mjs +7 -2
- package/lib/assets/line-1.mp3 +0 -0
- package/lib/assets/line-2.mp3 +0 -0
- package/lib/assets/line-3.mp3 +0 -0
- package/lib/assets/line-4.mp3 +0 -0
- package/lib/assets/line-5.mp3 +0 -0
- package/lib/assets/line-6.mp3 +0 -0
package/lib/client.js
CHANGED
|
@@ -249,10 +249,11 @@ window.__ModuleLoader__.load({
|
|
|
249
249
|
面板也要能正常显示这些控件,而不是一片 undefined。 */
|
|
250
250
|
var FALLBACK = {
|
|
251
251
|
voice: true, voiceLang: 'en', voiceName: '', voiceRate: 0.88, voicePitch: 0.85,
|
|
252
|
-
voiceVolume: 0.9, voiceCue: true, voiceLead: 240, voiceSource: 'asset',
|
|
252
|
+
voiceVolume: 0.9, voiceCue: true, voiceLead: 240, welcomeDelay: 550, voiceSource: 'asset', voicePack: 'f', voiceFx: 'robot',
|
|
253
253
|
sound: true, soundVolume: 0.5, bedNoise: false,
|
|
254
254
|
transitionIn: 'dissolve', transitionJoin: 'dissolve', transitionOut: 'dissolve',
|
|
255
|
-
transitionMs: 600, transitionDir: 'left'
|
|
255
|
+
transitionMs: 600, transitionDir: 'left',
|
|
256
|
+
colorMode: 'color'
|
|
256
257
|
}
|
|
257
258
|
function num(v, d) {
|
|
258
259
|
var n = Number(v)
|
|
@@ -265,7 +266,9 @@ window.__ModuleLoader__.load({
|
|
|
265
266
|
—— 桌面端走浏览器半侧自举时没有 1,就靠这一条
|
|
266
267
|
3) 下面这个常量:两份都没拿到时的兜底,别让面板显示 undefined
|
|
267
268
|
版本号本身只在 lib/index.js 里读一次,这里不重复维护。 */
|
|
268
|
-
var FALLBACK_VERSION = '2.13.
|
|
269
|
+
var FALLBACK_VERSION = '2.13.19'
|
|
270
|
+
/* 手动选包成功后置 true:让还在后台跑的“浏览器下载监听”不要再弹第二次更新器 */
|
|
271
|
+
var __manualUpdateApplied = false
|
|
269
272
|
function readInjectedVersion() {
|
|
270
273
|
try {
|
|
271
274
|
var v = window.__DSH_STARTUP_VERSION__
|
|
@@ -283,10 +286,13 @@ window.__ModuleLoader__.load({
|
|
|
283
286
|
var [busy, setBusy] = react.useState(false)
|
|
284
287
|
var [templates, setTemplates] = react.useState([])
|
|
285
288
|
var [hiddenTemplates, setHiddenTemplates] = react.useState([])
|
|
289
|
+
var [voicePacks, setVoicePacks] = react.useState([])
|
|
286
290
|
var [update, setUpdate] = react.useState(null)
|
|
287
291
|
var [checking, setChecking] = react.useState(false)
|
|
288
292
|
var [updating, setUpdating] = react.useState(false)
|
|
289
293
|
var [updateMsg, setUpdateMsg] = react.useState('')
|
|
294
|
+
var [waitingDownload, setWaitingDownload] = react.useState(false)
|
|
295
|
+
var [diagnosing, setDiagnosing] = react.useState(false)
|
|
290
296
|
|
|
291
297
|
function load() {
|
|
292
298
|
setError(null)
|
|
@@ -317,6 +323,25 @@ window.__ModuleLoader__.load({
|
|
|
317
323
|
}
|
|
318
324
|
react.useEffect(() => { refreshTemplates() }, [])
|
|
319
325
|
|
|
326
|
+
/* 音色包列表:来自 lib/assets/voices/manifest.json,由宿主只读下发 */
|
|
327
|
+
function refreshVoices() {
|
|
328
|
+
return fetch('/dsh-startup/voices', { headers: { Accept: 'application/json' }, cache: 'no-store' })
|
|
329
|
+
.then((r) => r.json())
|
|
330
|
+
.then((j) => { if (j && j.ok === true && Array.isArray(j.packs)) setVoicePacks(j.packs) })
|
|
331
|
+
.catch(() => { /* 拉不到就只显示内置音色 */ })
|
|
332
|
+
}
|
|
333
|
+
react.useEffect(() => { refreshVoices() }, [])
|
|
334
|
+
|
|
335
|
+
function voiceOptions() {
|
|
336
|
+
var opts = []
|
|
337
|
+
for (var i = 0; i < voicePacks.length; i++) {
|
|
338
|
+
var v = voicePacks[i]
|
|
339
|
+
if (!v || !v.id) continue
|
|
340
|
+
opts.push([v.id, String(v.name || v.id)])
|
|
341
|
+
}
|
|
342
|
+
return opts
|
|
343
|
+
}
|
|
344
|
+
|
|
320
345
|
function set(key, value) {
|
|
321
346
|
setNotice(null)
|
|
322
347
|
setDraft((d) => Object.assign({}, d, { [key]: value }))
|
|
@@ -357,18 +382,23 @@ window.__ModuleLoader__.load({
|
|
|
357
382
|
.then(() => setBusy(false))
|
|
358
383
|
}
|
|
359
384
|
|
|
360
|
-
/**
|
|
385
|
+
/** 一键诊断:让宿主写一份报告到桌面,用户直接发给作者。生成中按钮禁用,防止连点刷屏。 */
|
|
361
386
|
function runDiagnose() {
|
|
362
|
-
|
|
387
|
+
if (diagnosing) return
|
|
388
|
+
setDiagnosing(true)
|
|
389
|
+
setNotice('正在生成诊断报告,请稍等…')
|
|
390
|
+
setError(null)
|
|
363
391
|
fetch(STARTUP_ROUTE + '/diagnose', { method: 'POST' })
|
|
364
392
|
.then(function (r) { return r.json() })
|
|
365
393
|
.then(function (j) {
|
|
366
394
|
if (!j || j.ok !== true) throw new Error((j && j.error) || '生成失败')
|
|
367
|
-
setNotice('诊断报告已生成到桌面:' + j.file)
|
|
395
|
+
setNotice('诊断报告已生成到桌面:' + j.file + '(报告里不含任何密钥/密码)')
|
|
368
396
|
})
|
|
369
397
|
.catch(function (e) {
|
|
370
|
-
setNotice(
|
|
398
|
+
setNotice(null)
|
|
399
|
+
setError('生成诊断报告失败:' + String((e && e.message) || e))
|
|
371
400
|
})
|
|
401
|
+
.then(function () { setDiagnosing(false) })
|
|
372
402
|
}
|
|
373
403
|
|
|
374
404
|
/** 检查更新:问宿主(宿主去读 GitHub latest release,避开浏览器 CORS)。 */
|
|
@@ -409,27 +439,44 @@ window.__ModuleLoader__.load({
|
|
|
409
439
|
|
|
410
440
|
/** 立即更新:让系统浏览器下载(走浏览器自己的代理),插件监听 Downloads 自动接住 */
|
|
411
441
|
function runUpdate() {
|
|
412
|
-
if (!update || !update.latest) return
|
|
442
|
+
if (!update || !update.latest || updating) return
|
|
443
|
+
__manualUpdateApplied = false
|
|
413
444
|
setUpdating(true)
|
|
445
|
+
setWaitingDownload(true)
|
|
414
446
|
setUpdateMsg('正在让浏览器下载 v' + update.latest + '(使用浏览器自己的代理)…')
|
|
415
447
|
fetch(STARTUP_ROUTE + '/update-browser-download', { method: 'POST' })
|
|
416
448
|
.then(function (r) { return r.json() })
|
|
417
449
|
.then(function (j) {
|
|
418
450
|
if (!j || j.ok !== true) throw new Error((j && j.error) || '无法启动浏览器下载')
|
|
419
|
-
setUpdateMsg('已用 ' + (j.browser === 'chrome' ? 'Chrome' : '默认浏览器') + ' 打开下载,完成后会自动继续(最多等
|
|
451
|
+
setUpdateMsg('已用 ' + (j.browser === 'chrome' ? 'Chrome' : '默认浏览器') + ' 打开下载,完成后会自动继续(最多等 30 分钟)。等太久可以点「打开下载文件夹」或「选择安装包」。')
|
|
420
452
|
return fetch(STARTUP_ROUTE + '/update-watch-download?version=' + encodeURIComponent(update.latest))
|
|
421
453
|
})
|
|
422
454
|
.then(function (r) { return r.json() })
|
|
423
455
|
.then(function (j) {
|
|
456
|
+
setWaitingDownload(false)
|
|
457
|
+
if (__manualUpdateApplied) return
|
|
424
458
|
if (!j || j.ok !== true) throw new Error((j && j.error) || '没有等到下载文件')
|
|
425
459
|
setUpdating(false)
|
|
426
460
|
setUpdateMsg('已获取 v' + j.version + '(' + Math.round(j.size / 1048576) + ' MB)')
|
|
427
461
|
confirmAndApply(j.version)
|
|
428
462
|
})
|
|
429
463
|
.catch(function (e) {
|
|
464
|
+
setWaitingDownload(false)
|
|
465
|
+
if (__manualUpdateApplied) return
|
|
430
466
|
setUpdating(false)
|
|
431
|
-
setUpdateMsg('浏览器下载没接住:' + String((e && e.message) || e) + '
|
|
467
|
+
setUpdateMsg('浏览器下载没接住:' + String((e && e.message) || e) + '。可以点「打开下载文件夹」找到 zip 后点「选择安装包」。')
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** 打开系统下载文件夹(等下载时的手动出口) */
|
|
472
|
+
function openDownloads() {
|
|
473
|
+
fetch(STARTUP_ROUTE + '/open-downloads', { method: 'POST' })
|
|
474
|
+
.then(function (r) { return r.json() })
|
|
475
|
+
.then(function (j) {
|
|
476
|
+
if (!j || j.ok !== true) throw new Error((j && j.error) || '打不开文件夹')
|
|
477
|
+
setUpdateMsg('已打开下载文件夹:' + j.dir + '。找到 dsh-boot-suite 的 zip 后,点「选择安装包」选它。')
|
|
432
478
|
})
|
|
479
|
+
.catch(function (e) { setUpdateMsg('打开下载文件夹失败:' + String((e && e.message) || e)) })
|
|
433
480
|
}
|
|
434
481
|
|
|
435
482
|
/** 浏览器下载好 zip 后,从本地选择文件(完全不依赖宿主联网) */
|
|
@@ -454,6 +501,8 @@ window.__ModuleLoader__.load({
|
|
|
454
501
|
.then(function (r) { return r.json() })
|
|
455
502
|
.then(function (j) {
|
|
456
503
|
if (!j || j.ok !== true) throw new Error((j && j.error) || '本地安装包无效')
|
|
504
|
+
__manualUpdateApplied = true
|
|
505
|
+
setWaitingDownload(false)
|
|
457
506
|
setUpdating(false)
|
|
458
507
|
setUpdateMsg('已载入本地安装包 v' + j.version + '(' + Math.round(j.size / 1048576) + ' MB)')
|
|
459
508
|
confirmAndApply(j.version)
|
|
@@ -616,7 +665,10 @@ window.__ModuleLoader__.load({
|
|
|
616
665
|
.then((r) => r.json())
|
|
617
666
|
.then((j) => {
|
|
618
667
|
if (!j || j.ok !== true) return
|
|
619
|
-
|
|
668
|
+
/* v2.13.15:预览必须带上「当前还没保存的修改」。
|
|
669
|
+
原来只灌宿主已保存的配置:用户选完音色/效果直接点「立即预览」,
|
|
670
|
+
运行时拿到的还是旧配置 —— 表现就是「选了日文音色,预览仍播英文」。 */
|
|
671
|
+
if (j.config) window.__DSH_STARTUP__ = Object.assign({}, FALLBACK, j.config, draft || {})
|
|
620
672
|
try {
|
|
621
673
|
if (j.template) window.__DSH_STARTUP_TEMPLATE__ = j.template
|
|
622
674
|
else delete window.__DSH_STARTUP_TEMPLATE__
|
|
@@ -876,56 +928,38 @@ window.__ModuleLoader__.load({
|
|
|
876
928
|
h('div', { className: '__ss_groupTitle' }, '语音与音效'),
|
|
877
929
|
h(Check, {
|
|
878
930
|
label: '女声播报(念每一行的英文)',
|
|
879
|
-
hint: '
|
|
880
|
-
+ '动画会等这句话念完再走下一步,不会把语音掐断;浏览器要求先有一次点击才准出声,'
|
|
881
|
-
+ '所以第一次点按钮/按 Enter 时会把当前这行补念一次。',
|
|
931
|
+
hint: '每一行的英文都在文字出现的同一时刻念出来,动画等这句话念完再走下一步。',
|
|
882
932
|
checked: draft.voice,
|
|
883
933
|
onChange: (v) => set('voice', v)
|
|
884
934
|
}),
|
|
885
|
-
h(Field, {
|
|
886
|
-
label: '
|
|
887
|
-
hint: '
|
|
888
|
-
+ '
|
|
935
|
+
draft.voice ? h(Field, {
|
|
936
|
+
label: '配音音色',
|
|
937
|
+
hint: '6 套完整干音:英文 1 套(音色F·默认)+ 日文 5 套(日文音色1–5);可再叠声音效果。'
|
|
938
|
+
+ '选完音色后,下面的「声音效果」可以再叠一层处理,或选「无」保留原声。'
|
|
889
939
|
},
|
|
890
940
|
h(Select, {
|
|
891
|
-
value: draft.
|
|
892
|
-
options:
|
|
893
|
-
onChange: (v) => set('
|
|
941
|
+
value: draft.voicePack || 'builtin',
|
|
942
|
+
options: voiceOptions(),
|
|
943
|
+
onChange: (v) => set('voicePack', v)
|
|
894
944
|
})
|
|
895
|
-
),
|
|
896
|
-
h(Field, {
|
|
945
|
+
) : null,
|
|
946
|
+
draft.voice ? h(Field, {
|
|
947
|
+
label: '声音效果',
|
|
948
|
+
hint: '5 个音效都来自模板编辑器里的同一套参数,实时叠加在所选音色上;选「无(原声)」就是干净人声。'
|
|
949
|
+
+ '只影响全局配音素材,模板自带的配音不受影响。'
|
|
950
|
+
},
|
|
897
951
|
h(Select, {
|
|
898
|
-
value: draft.
|
|
899
|
-
options: [
|
|
900
|
-
|
|
952
|
+
value: draft.voiceFx || 'none',
|
|
953
|
+
options: [
|
|
954
|
+
['none', '无(原声,默认)'],
|
|
955
|
+
['ai-mech', 'AI 机械音'],
|
|
956
|
+
['robot', '机器人'],
|
|
957
|
+
['radio', '无线电'],
|
|
958
|
+
['oldtv', '老电视'],
|
|
959
|
+
['dark', '低沉']
|
|
960
|
+
],
|
|
961
|
+
onChange: (v) => set('voiceFx', v)
|
|
901
962
|
})
|
|
902
|
-
),
|
|
903
|
-
draft.voice ? h(VoicePicker, {
|
|
904
|
-
value: draft.voiceName,
|
|
905
|
-
onChange: (v) => set('voiceName', v),
|
|
906
|
-
onPreview: () => speakSample(null, draft)
|
|
907
|
-
}) : null,
|
|
908
|
-
draft.voice ? h('div', { className: '__ss_row' },
|
|
909
|
-
h('button', {
|
|
910
|
-
className: '__ss_btn',
|
|
911
|
-
type: 'button',
|
|
912
|
-
onClick: () => {
|
|
913
|
-
set('voiceRate', 0.88)
|
|
914
|
-
set('voicePitch', 0.85)
|
|
915
|
-
set('voiceName', '')
|
|
916
|
-
set('voiceCue', true)
|
|
917
|
-
set('voiceLead', 240)
|
|
918
|
-
}
|
|
919
|
-
}, '↺ 套用「电影旁白」档'),
|
|
920
|
-
h('span', { className: '__ss_hint' }, '语速 0.88 · 音调 0.85(压抑、深沉)· 自动挑神经网络女声 · 提示音 + 开口留白')
|
|
921
|
-
) : null,
|
|
922
|
-
draft.voice ? h('div', { className: '__ss_two' },
|
|
923
|
-
h(Field, { label: '语速' },
|
|
924
|
-
h(Range, { value: num(draft.voiceRate, 1), min: 0.5, max: 2, display: num(draft.voiceRate, 1).toFixed(2) + '×', onChange: (v) => set('voiceRate', v) })
|
|
925
|
-
),
|
|
926
|
-
h(Field, { label: '音调' },
|
|
927
|
-
h(Range, { value: num(draft.voicePitch, 1), min: 0, max: 2, display: num(draft.voicePitch, 1).toFixed(2), onChange: (v) => set('voicePitch', v) })
|
|
928
|
-
)
|
|
929
963
|
) : null,
|
|
930
964
|
draft.voice ? h(Field, { label: '开口前留白', hint: '提示音响过之后停多久再开口。电影旁白/机场广播都有这一拍,240ms 起最像"系统在播报"。' },
|
|
931
965
|
h(Range, {
|
|
@@ -934,6 +968,13 @@ window.__ModuleLoader__.load({
|
|
|
934
968
|
onChange: (v) => set('voiceLead', v)
|
|
935
969
|
})
|
|
936
970
|
) : null,
|
|
971
|
+
draft.voice ? h(Field, { label: '终幕语音延迟', hint: '05→06 的过场和声钟约 1.5 秒;默认等它收尾再进最后一句。觉得欢迎语进得太早/太晚,就调这里。' },
|
|
972
|
+
h(Range, {
|
|
973
|
+
value: num(draft.welcomeDelay, 550), min: 0, max: 3000, step: 50,
|
|
974
|
+
display: Math.round(num(draft.welcomeDelay, 550)) + ' ms',
|
|
975
|
+
onChange: (v) => set('welcomeDelay', v)
|
|
976
|
+
})
|
|
977
|
+
) : null,
|
|
937
978
|
draft.voice ? h(Field, { label: '音量' },
|
|
938
979
|
h(Range, { value: num(draft.voiceVolume, 0.9), min: 0, max: 1, display: Math.round(num(draft.voiceVolume, 0.9) * 100) + '%', onChange: (v) => set('voiceVolume', v) })
|
|
939
980
|
) : null,
|
|
@@ -1017,7 +1058,7 @@ window.__ModuleLoader__.load({
|
|
|
1017
1058
|
/* 版本与更新 */
|
|
1018
1059
|
h('div', { className: '__ss_row', style: { marginTop: '10px' } },
|
|
1019
1060
|
h('button', { className: '__ss_btn', disabled: checking, onClick: checkUpdate }, checking ? '检查中…' : '检查更新'),
|
|
1020
|
-
h('button', { className: '__ss_btn', onClick: runDiagnose }, '生成诊断报告'),
|
|
1061
|
+
h('button', { className: '__ss_btn', disabled: diagnosing, onClick: runDiagnose }, diagnosing ? '生成中…' : '生成诊断报告'),
|
|
1021
1062
|
h('span', {
|
|
1022
1063
|
className: (update && update.ok && update.hasUpdate) ? '__ss_ok' : '__ss_hint'
|
|
1023
1064
|
}, (function () {
|
|
@@ -1034,12 +1075,19 @@ window.__ModuleLoader__.load({
|
|
|
1034
1075
|
? h('button', { className: '__ss_btn __ss_btnPrimary', disabled: updating, onClick: runUpdate }, updating ? '更新中…' : '立即更新')
|
|
1035
1076
|
: null,
|
|
1036
1077
|
(update && update.ok && update.hasUpdate)
|
|
1037
|
-
? h('button', { className: '__ss_btn', disabled: updating, onClick: pickLocalPackage }, '选择安装包')
|
|
1078
|
+
? h('button', { className: '__ss_btn', disabled: updating && !waitingDownload, onClick: pickLocalPackage }, '选择安装包')
|
|
1079
|
+
: null,
|
|
1080
|
+
(update && update.ok && update.hasUpdate && waitingDownload)
|
|
1081
|
+
? h('button', { className: '__ss_btn', onClick: openDownloads }, '打开下载文件夹')
|
|
1038
1082
|
: null
|
|
1039
1083
|
),
|
|
1040
1084
|
(update && update.ok && update.hasUpdate)
|
|
1041
1085
|
? h('div', { className: '__ss_hint', style: { marginTop: '6px', lineHeight: '1.8' } },
|
|
1042
|
-
'下载会优先用 Chrome(Edge + IDM
|
|
1086
|
+
'下载会优先用 Chrome(Edge + IDM 可能拦下载)。如果没自动继续,点「打开下载文件夹」找到 zip,再点「选择安装包」。')
|
|
1087
|
+
: null,
|
|
1088
|
+
waitingDownload
|
|
1089
|
+
? h('div', { className: '__ss_hint', style: { marginTop: '6px', lineHeight: '1.8', color: '#c98a2b' } },
|
|
1090
|
+
'正在等浏览器下载(最多 30 分钟)。下载完成后会自动继续;如果已经下好,点「打开下载文件夹」看看,或直接点「选择安装包」选那个 zip。')
|
|
1043
1091
|
: null,
|
|
1044
1092
|
h('div', { className: '__ss_hint', style: { marginTop: '6px', lineHeight: '1.8' } },
|
|
1045
1093
|
'回滚保护:安装新版本前,桌面会自动多一个「紧急回滚-V当前版本」按钮。新版装不上、或装完 DSH 打不开时,完全退出 DSH(窗口+托盘)后双击它,就能回到更新前的版本;它只换插件版本,不动你的设置。更新成功后旧按钮会被自动替换,不会越积越多——确认新版正常后也不用手动删。'),
|
|
@@ -1552,7 +1600,7 @@ window.__ModuleLoader__.load({
|
|
|
1552
1600
|
/* 每次改客户端就把这个号 +1。
|
|
1553
1601
|
有了它在控制台一眼就能判断"浏览器里跑的是不是最新那份" ——
|
|
1554
1602
|
排查问题时,分不清是代码问题还是旧包,会白猜一轮。 */
|
|
1555
|
-
var BUILD = 'cw-
|
|
1603
|
+
var BUILD = 'cw-42'
|
|
1556
1604
|
|
|
1557
1605
|
function log() {
|
|
1558
1606
|
try {
|
|
@@ -2058,6 +2106,8 @@ window.__ModuleLoader__.load({
|
|
|
2058
2106
|
L.push(B + ' > ' + V + ' { position: absolute; inset: 0; }')
|
|
2059
2107
|
L.push('html.dsh-cw-light ' + B + ' > ' + V + ' { background: linear-gradient(' + cfg.veilLightA + ', ' + cfg.veilLightB + '); }')
|
|
2060
2108
|
L.push('html.dsh-cw-dark ' + B + ' > ' + V + ' { background: linear-gradient(' + cfg.veilDarkA + ', ' + cfg.veilDarkB + '); }')
|
|
2109
|
+
/* v2.13.17:黑白原版素材模式 —— 不染色,直接关掉主题色纱(模糊仍可单独调) */
|
|
2110
|
+
L.push('html.dsh-cw-mono ' + B + ' > ' + V + ' { background: transparent !important; }')
|
|
2061
2111
|
/* v2.13.11 镜头模糊:只模糊纱后面的壁纸/视频,纱上的聊天文字不受影响。
|
|
2062
2112
|
0 = 不生成规则(完全不动);100 = 约 24px。 */
|
|
2063
2113
|
if (Number(cfg.veilBlur) > 0) {
|
|
@@ -2353,6 +2403,8 @@ window.__ModuleLoader__.load({
|
|
|
2353
2403
|
current = cfg
|
|
2354
2404
|
var dark = looksDark()
|
|
2355
2405
|
setThemeClass(dark)
|
|
2406
|
+
/* v2.13.17:黑白(原版素材)模式 → 关掉主题色纱,保留原画面 */
|
|
2407
|
+
try { document.documentElement.classList.toggle('dsh-cw-mono', cfg.colorMode === 'mono') } catch (e) { /* 忽略 */ }
|
|
2356
2408
|
log('套用壁纸', { image: cfg.__image, theme: dark ? 'dark' : 'light' })
|
|
2357
2409
|
injectCss(cfg)
|
|
2358
2410
|
mountBg(cfg) /* 最底:聊天底图(用户自己定的那张,可选)*/
|
|
@@ -2386,7 +2438,7 @@ window.__ModuleLoader__.load({
|
|
|
2386
2438
|
removeBg()
|
|
2387
2439
|
removeBanner()
|
|
2388
2440
|
try {
|
|
2389
|
-
document.documentElement.classList.remove('dsh-cw-dark', 'dsh-cw-light')
|
|
2441
|
+
document.documentElement.classList.remove('dsh-cw-dark', 'dsh-cw-light', 'dsh-cw-mono')
|
|
2390
2442
|
themeClass = null
|
|
2391
2443
|
} catch (e) { /* 忽略 */ }
|
|
2392
2444
|
}
|
|
@@ -2520,9 +2572,11 @@ window.__ModuleLoader__.load({
|
|
|
2520
2572
|
var [busy, setBusy] = cwReact.useState(false)
|
|
2521
2573
|
var [error, setError] = cwReact.useState(null)
|
|
2522
2574
|
var [notice, setNotice] = cwReact.useState(null)
|
|
2575
|
+
var [refreshing, setRefreshing] = cwReact.useState(false)
|
|
2523
2576
|
|
|
2524
|
-
function load() {
|
|
2577
|
+
function load(manual) {
|
|
2525
2578
|
setError(null)
|
|
2579
|
+
if (manual) { setNotice(null); setRefreshing(true) }
|
|
2526
2580
|
return Promise.all([api('/images'), api('/config', { cache: 'no-store' }), api('/videos')])
|
|
2527
2581
|
.then(function (arr) {
|
|
2528
2582
|
var images = arr[0] || {}
|
|
@@ -2543,6 +2597,7 @@ window.__ModuleLoader__.load({
|
|
|
2543
2597
|
veilBlur: cfg.veilBlur == null ? 0 : Math.max(0, Math.min(100, Number(cfg.veilBlur) || 0)),
|
|
2544
2598
|
image: conf.image || null,
|
|
2545
2599
|
imageFit: cfg.imageFit === 'contain' ? 'contain' : 'cover',
|
|
2600
|
+
colorMode: cfg.colorMode === 'mono' ? 'mono' : 'color',
|
|
2546
2601
|
video: {
|
|
2547
2602
|
bgMode: vids.bgMode || 'image',
|
|
2548
2603
|
videoMode: vids.videoMode || 'single',
|
|
@@ -2555,8 +2610,13 @@ window.__ModuleLoader__.load({
|
|
|
2555
2610
|
}
|
|
2556
2611
|
setData(next)
|
|
2557
2612
|
setDraft({ banner: next.banner, bannerOpacity: next.bannerOpacity, dark: looksDark(), newRoot: '', shuffleMin: next.video.shuffleMinutes })
|
|
2613
|
+
if (manual) {
|
|
2614
|
+
setNotice('已重新读取:图片 ' + next.images.length + ' 张 / 视频 ' + next.video.items.length + ' 个'
|
|
2615
|
+
+ (next.dropDirExists ? '' : '(丢图目录还不存在,点「打开图片文件夹」会创建)'))
|
|
2616
|
+
}
|
|
2558
2617
|
})
|
|
2559
2618
|
.catch(function (e) { setError('读取失败:' + String((e && e.message) || e)) })
|
|
2619
|
+
.then(function () { if (manual) setRefreshing(false) })
|
|
2560
2620
|
}
|
|
2561
2621
|
cwReact.useEffect(function () { load() }, [])
|
|
2562
2622
|
|
|
@@ -2659,11 +2719,20 @@ window.__ModuleLoader__.load({
|
|
|
2659
2719
|
cwH('br'),
|
|
2660
2720
|
'循环视频:' + (data.video && data.video.items.length ? ('已扫描 ' + data.video.items.length + ' 段' + (data.video.bgMode === 'video' && data.video.currentId ? ',正在播放' : ',未启用')) : '(视频目录里还没有视频)'),
|
|
2661
2721
|
cwH('br'),
|
|
2662
|
-
'丢图目录:' + (data.dropDir || '
|
|
2722
|
+
'丢图目录:' + (data.dropDir || '(尚未创建,点「打开图片文件夹」会自动创建)')
|
|
2663
2723
|
),
|
|
2664
2724
|
cwH('div', { className: '__ss_row' },
|
|
2665
|
-
cwH('button', {
|
|
2666
|
-
|
|
2725
|
+
cwH('button', {
|
|
2726
|
+
className: '__ss_btn', type: 'button', disabled: busy,
|
|
2727
|
+
onClick: function () {
|
|
2728
|
+
setNotice(null); setError(null)
|
|
2729
|
+
api('/open-dir', { method: 'POST' }).then(function (j) {
|
|
2730
|
+
if (j && j.ok) setNotice('已打开图片文件夹:' + (j.opened || data.dropDir))
|
|
2731
|
+
else setError('打开图片文件夹失败:' + ((j && j.error) || '宿主没有返回'))
|
|
2732
|
+
})
|
|
2733
|
+
}
|
|
2734
|
+
}, '打开图片文件夹'),
|
|
2735
|
+
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy || refreshing, onClick: function () { load(true) } }, refreshing ? '刷新中…' : '刷新')
|
|
2667
2736
|
)
|
|
2668
2737
|
),
|
|
2669
2738
|
|
|
@@ -2672,7 +2741,7 @@ window.__ModuleLoader__.load({
|
|
|
2672
2741
|
cwH('div', { className: '__ss_hint' }, '点一下立刻换。第一张「默认背景(内置)」是插件自带、开箱就有的;把图丢进上面的目录也会出现在这里,按修改时间取最新那张。'),
|
|
2673
2742
|
data.images.length
|
|
2674
2743
|
? cwH('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, data.images.map(thumb))
|
|
2675
|
-
: cwH('div', { className: '__ss_hint' }, '目录里还没有图。点「打开图片文件夹」,把 jpg / png / webp
|
|
2744
|
+
: cwH('div', { className: '__ss_hint' }, '目录里还没有图。点「打开图片文件夹」,把 jpg / png / webp 丢进去,再点刷新。只扫描这一层,子文件夹里的不算(支持 jpg/png/webp/gif/avif/bmp)。'),
|
|
2676
2745
|
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
2677
2746
|
cwH('span', { className: '__ss_status' }, '图片适配'),
|
|
2678
2747
|
cwH('select', {
|
|
@@ -2714,6 +2783,18 @@ window.__ModuleLoader__.load({
|
|
|
2714
2783
|
cwH('option', { value: 'shuffle' }, '随机轮换')
|
|
2715
2784
|
)
|
|
2716
2785
|
),
|
|
2786
|
+
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
2787
|
+
cwH('span', { className: '__ss_status' }, '色彩模式'),
|
|
2788
|
+
cwH('select', {
|
|
2789
|
+
className: '__ss_select', disabled: busy,
|
|
2790
|
+
value: data.colorMode,
|
|
2791
|
+
onChange: function (ev) { apply({ colorMode: ev.target.value }, true) }
|
|
2792
|
+
},
|
|
2793
|
+
cwH('option', { value: 'color' }, '彩色(默认)'),
|
|
2794
|
+
cwH('option', { value: 'mono' }, '黑白(原版素材,不染色)')
|
|
2795
|
+
)
|
|
2796
|
+
),
|
|
2797
|
+
cwH('div', { className: '__ss_hint' }, '黑白模式会关掉主题色纱、保留原画面颜色,适合本身就是黑白的动态壁纸;彩色模式不做任何调色。'),
|
|
2717
2798
|
data.video.videoMode === 'shuffle'
|
|
2718
2799
|
? cwH('div', { className: '__ss_row' },
|
|
2719
2800
|
cwH('span', { className: '__ss_status' }, '轮换间隔(分钟,0 = 每次启动随机)'),
|
|
@@ -2764,7 +2845,7 @@ window.__ModuleLoader__.load({
|
|
|
2764
2845
|
: cwH('div', { className: '__ss_hint' }, '还没有视频。把 mp4/webm 或壁纸引擎的项目文件夹放进上面的目录,再点下方「刷新」。'),
|
|
2765
2846
|
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
2766
2847
|
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy || !(data.video.roots && data.video.roots.length), onClick: function () { if (data.video.roots && data.video.roots.length) rootAction('open', data.video.roots[0]) } }, '打开视频文件夹'),
|
|
2767
|
-
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy, onClick: function () { load() } }, '刷新')
|
|
2848
|
+
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy || refreshing, onClick: function () { load(true) } }, refreshing ? '刷新中…' : '刷新')
|
|
2768
2849
|
)
|
|
2769
2850
|
),
|
|
2770
2851
|
|
package/lib/host-startup.js
CHANGED
|
@@ -19,6 +19,18 @@ import { spawn } from 'node:child_process'
|
|
|
19
19
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
20
|
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
21
21
|
const CONFIG_FILE = path.join(DSH_HOME, 'dsh-startup.json')
|
|
22
|
+
const VOICE_ROOT = path.join(PACKAGE_ROOT, 'lib', 'assets', 'voices')
|
|
23
|
+
/* 音色包清单由 lib/assets/voices/manifest.json 描述(随安装包发布)。
|
|
24
|
+
读不到就当作没有扩展音色,内置音色不受影响。 */
|
|
25
|
+
const VOICE_PACKS = (() => {
|
|
26
|
+
try {
|
|
27
|
+
const list = JSON.parse(fs.readFileSync(path.join(VOICE_ROOT, 'manifest.json'), 'utf8'))
|
|
28
|
+
if (!Array.isArray(list)) return []
|
|
29
|
+
return list.filter((v) => v && /^[a-z]$/.test(String(v.id)) && (v.lang === 'en' || v.lang === 'ja'))
|
|
30
|
+
.map((v) => ({ id: String(v.id), name: String(v.name || '').slice(0, 12) || ('音色 ' + String(v.id).toUpperCase()), lang: v.lang }))
|
|
31
|
+
} catch (err) { return [] }
|
|
32
|
+
})()
|
|
33
|
+
const VOICE_PACK_IDS = [...VOICE_PACKS.map((v) => v.id)]
|
|
22
34
|
|
|
23
35
|
const NS = 'dsh-startup-screen'
|
|
24
36
|
const ROUTE_BASE = '/dsh-startup'
|
|
@@ -75,7 +87,10 @@ const DEFAULTS = {
|
|
|
75
87
|
voiceCue: true, // 念台词前的一声轻提示
|
|
76
88
|
sfx: 'audio', // 音效来源:audio=lib/assets 里的真素材 / synth=现场合成
|
|
77
89
|
voiceSource: 'asset', // 语音来源:asset=优先用真声素材(拿不到自动回落 TTS)/ tts=全 TTS
|
|
90
|
+
voicePack: 'f', // 配音音色:f..k=音色包(默认 F 英文);用户在设置里可换
|
|
91
|
+
voiceFx: 'robot', // 声音效果:none | ai-mech | robot | radio | oldtv | dark(默认机器人,贴近旧版听感)
|
|
78
92
|
voiceLead: 240, // 提示音之后留一小段空白再开口
|
|
93
|
+
welcomeDelay: 550, // 05→06 过场和声钟之后等多久再进终幕欢迎语(0–3000ms;实测 550 最顺)
|
|
79
94
|
identity: 'JOYCE MOORE', // ← 设置页里可改的「身份名称」
|
|
80
95
|
identityId: '0087',
|
|
81
96
|
accessLevel: '3',
|
|
@@ -117,6 +132,8 @@ const ENUMS = {
|
|
|
117
132
|
voiceLang: ['en', 'zh', 'both'],
|
|
118
133
|
sfx: ['audio', 'synth'],
|
|
119
134
|
voiceSource: ['asset', 'tts'],
|
|
135
|
+
voicePack: VOICE_PACK_IDS,
|
|
136
|
+
voiceFx: ['none', 'ai-mech', 'robot', 'radio', 'oldtv', 'dark'],
|
|
120
137
|
transitionIn: TRANSITION_TYPES,
|
|
121
138
|
transitionJoin: TRANSITION_TYPES,
|
|
122
139
|
transitionOut: TRANSITION_TYPES,
|
|
@@ -125,6 +142,7 @@ const ENUMS = {
|
|
|
125
142
|
const BOOL_KEYS = ['enabled', 'requireInteraction', 'allowSkip', 'sound', 'bedNoise', 'voice', 'voiceCue']
|
|
126
143
|
/** 数值字段的合法区间(越界即夹取)。 */
|
|
127
144
|
const RANGES = {
|
|
145
|
+
welcomeDelay: [0, 3000],
|
|
128
146
|
speed: [0.35, 3],
|
|
129
147
|
transitionMs: [200, 2000],
|
|
130
148
|
soundVolume: [0, 1],
|
|
@@ -397,6 +415,36 @@ function apply(ctx) {
|
|
|
397
415
|
}
|
|
398
416
|
}))
|
|
399
417
|
|
|
418
|
+
/* 音色包:6 套扩展配音(英文 F + 日文 G–K)。设置页只拿 id/名称/语言,音频走只读白名单路由;
|
|
419
|
+
id 与文件名都白名单,且不接受子路径 —— 和 /asset 同样防目录穿越。 */
|
|
420
|
+
disposers.push(registerRoute({
|
|
421
|
+
kind: 'exact',
|
|
422
|
+
path: ROUTE_BASE + '/voices',
|
|
423
|
+
handler: (req, res) => sendJson(res, 200, { ok: true, packs: VOICE_PACKS, default: 'f' })
|
|
424
|
+
}))
|
|
425
|
+
disposers.push(registerRoute({
|
|
426
|
+
kind: 'prefix',
|
|
427
|
+
path: ROUTE_BASE + '/voice',
|
|
428
|
+
handler: (req, res) => {
|
|
429
|
+
const rest = decodeURIComponent(String(req.url || '').split('?')[0])
|
|
430
|
+
.slice((ROUTE_BASE + '/voice').length).replace(/^\/+/, '')
|
|
431
|
+
const parts = rest.split('/')
|
|
432
|
+
const id = String(parts[0] || '')
|
|
433
|
+
const name = String(parts[1] || '')
|
|
434
|
+
if (!/^[a-z]$/.test(id) || !/^line-[1-6]\.mp3$/.test(name) || !VOICE_PACK_IDS.includes(id)) {
|
|
435
|
+
res.writeHead(404); res.end(); return
|
|
436
|
+
}
|
|
437
|
+
let bytes
|
|
438
|
+
try { bytes = fs.readFileSync(path.join(VOICE_ROOT, id, name)) } catch (err) { res.writeHead(404); res.end(); return }
|
|
439
|
+
res.writeHead(200, {
|
|
440
|
+
'Content-Type': 'audio/mpeg',
|
|
441
|
+
'Cache-Control': 'no-store',
|
|
442
|
+
'Content-Length': String(bytes.length)
|
|
443
|
+
})
|
|
444
|
+
res.end(bytes)
|
|
445
|
+
}
|
|
446
|
+
}))
|
|
447
|
+
|
|
400
448
|
/* 模板自带声音:/dsh-startup/template-asset/<模板id>/<文件名>
|
|
401
449
|
先找用户目录 ~/.dsh/startup-templates/<id>/assets/,再找插件自带 templates/<id>/assets/。
|
|
402
450
|
id 与文件名都过白名单,且不接受子路径 —— 模板作者塞不进来任意文件读取。 */
|
|
@@ -601,11 +649,16 @@ function apply(ctx) {
|
|
|
601
649
|
'<script src="' + ROUTE_BASE + '/splash.js"></script></body></html>'
|
|
602
650
|
}
|
|
603
651
|
|
|
604
|
-
/* 一键诊断:把环境信息写到桌面,让不会用 AI
|
|
652
|
+
/* 一键诊断:把环境信息写到桌面,让不会用 AI 的用户也能一键求助。
|
|
653
|
+
同一时间只允许生成一份:按钮连点时后面的请求直接返回“正在生成中”,
|
|
654
|
+
避免桌面被一堆同名报告刷屏。 */
|
|
655
|
+
let diagnoseBusy = false
|
|
605
656
|
disposers.push(registerRoute({
|
|
606
657
|
kind: 'exact',
|
|
607
658
|
path: ROUTE_BASE + '/diagnose',
|
|
608
659
|
handler: async (req, res) => {
|
|
660
|
+
if (diagnoseBusy) { sendJson(res, 429, { ok: false, error: '诊断报告正在生成中,请等几秒,不用重复点击。' }); return }
|
|
661
|
+
diagnoseBusy = true
|
|
609
662
|
try {
|
|
610
663
|
const lines = []
|
|
611
664
|
const add = (s2) => lines.push(s2 == null ? '' : String(s2))
|
|
@@ -688,6 +741,8 @@ function apply(ctx) {
|
|
|
688
741
|
sendJson(res, 200, { ok: true, file, desktop })
|
|
689
742
|
} catch (err) {
|
|
690
743
|
sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
744
|
+
} finally {
|
|
745
|
+
diagnoseBusy = false
|
|
691
746
|
}
|
|
692
747
|
}
|
|
693
748
|
}))
|
|
@@ -1327,6 +1382,26 @@ function apply(ctx) {
|
|
|
1327
1382
|
return out
|
|
1328
1383
|
}
|
|
1329
1384
|
|
|
1385
|
+
/* 打开浏览器默认下载目录:等下载超时/找不到文件时,给用户一个直接可见的出口 */
|
|
1386
|
+
disposers.push(registerRoute({
|
|
1387
|
+
kind: 'exact',
|
|
1388
|
+
path: ROUTE_BASE + '/open-downloads',
|
|
1389
|
+
handler: async (req, res) => {
|
|
1390
|
+
if ((req.method || 'GET').toUpperCase() !== 'POST') { sendJson(res, 405, { ok: false, error: 'method not allowed' }); return }
|
|
1391
|
+
try {
|
|
1392
|
+
const dir = browserDownloadDirs()[0]
|
|
1393
|
+
try { fs.mkdirSync(dir, { recursive: true }) } catch (e) { /* 忽略 */ }
|
|
1394
|
+
if (process.platform === 'win32') spawn('explorer.exe', [dir], { detached: true, stdio: 'ignore' }).unref()
|
|
1395
|
+
else if (process.platform === 'darwin') spawn('open', [dir], { detached: true, stdio: 'ignore' }).unref()
|
|
1396
|
+
else spawn('xdg-open', [dir], { detached: true, stdio: 'ignore' }).unref()
|
|
1397
|
+
diag('open-downloads ' + dir)
|
|
1398
|
+
sendJson(res, 200, { ok: true, dir })
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
sendJson(res, 200, { ok: false, error: String((err && err.message) || err) })
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}))
|
|
1404
|
+
|
|
1330
1405
|
|
|
1331
1406
|
/* 自动更新(浏览器代理路线):让系统默认浏览器去下载,插件只负责“接住”文件 */
|
|
1332
1407
|
disposers.push(registerRoute({
|
|
@@ -1366,7 +1441,8 @@ function apply(ctx) {
|
|
|
1366
1441
|
if (!/^[0-9][0-9A-Za-z.+-]{0,39}$/.test(version)) throw new Error('版本号不合法')
|
|
1367
1442
|
const dirs = browserDownloadDirs()
|
|
1368
1443
|
const since = (browserDownloadAt || Date.now()) - 120000
|
|
1369
|
-
|
|
1444
|
+
/* 30 分钟:国内网络下 24MB 的包偶尔要十几分钟,10 分钟太容易误判超时 */
|
|
1445
|
+
const deadline = Date.now() + 1800000
|
|
1370
1446
|
diag('update-watch-download start dirs=' + dirs.join('|') + ' since=' + since)
|
|
1371
1447
|
let found = null
|
|
1372
1448
|
let bad = null
|
|
@@ -1375,7 +1451,8 @@ function apply(ctx) {
|
|
|
1375
1451
|
try {
|
|
1376
1452
|
if (!fs.existsSync(dir)) continue
|
|
1377
1453
|
for (const f of fs.readdirSync(dir)) {
|
|
1378
|
-
|
|
1454
|
+
/* 文件名放宽:Chrome 可能存成 “dsh-boot-suite-win-V2.13.12 (1).zip” 甚至带空格的名字 */
|
|
1455
|
+
if (!/dsh[-_ ]?boot[-_ ]?suite.*\.zip$/i.test(f)) continue
|
|
1379
1456
|
const fp = path.join(dir, f)
|
|
1380
1457
|
const st = fs.statSync(fp)
|
|
1381
1458
|
if (!st.isFile() || st.size < 1024 * 1024) continue
|
|
@@ -1397,7 +1474,7 @@ function apply(ctx) {
|
|
|
1397
1474
|
}
|
|
1398
1475
|
if (!found) await new Promise((r) => setTimeout(r, 1500))
|
|
1399
1476
|
}
|
|
1400
|
-
if (!found) { diag('update-watch-download timeout'); sendJson(res, 200, { ok: false, error: '等待浏览器下载超时(
|
|
1477
|
+
if (!found) { diag('update-watch-download timeout'); sendJson(res, 200, { ok: false, error: '等待浏览器下载超时(30 分钟)。可以点「打开下载文件夹」看看安装包是不是已经下好,再点「选择安装包」手动指定。' }); return }
|
|
1401
1478
|
const raw = fs.readFileSync(found.file)
|
|
1402
1479
|
if (!zipComplete(raw)) throw new Error('浏览器下载的安装包不完整(zip 校验失败),请重新下载')
|
|
1403
1480
|
const outDir = path.join(DSH_HOME, 'boot-suite-updates')
|
package/lib/host-wallpaper.js
CHANGED
|
@@ -63,7 +63,8 @@ const DEFAULTS = {
|
|
|
63
63
|
videoMode: 'single', // single 手动选 / shuffle 随机轮换
|
|
64
64
|
videoId: '', // 选中的视频 id(视频文件的绝对路径规范化)
|
|
65
65
|
videoRoots: [], // 额外的视频存放目录(默认 loops 始终包含)
|
|
66
|
-
videoShuffleMinutes: 0
|
|
66
|
+
videoShuffleMinutes: 0, // 0 = 每次启动随机;>0 = 每 N 分钟换一段
|
|
67
|
+
colorMode: 'color' // color 彩色 | mono 黑白(原版素材,不染色)
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function readConfig() {
|
|
@@ -407,6 +408,8 @@ export default {
|
|
|
407
408
|
const cfg = readConfig()
|
|
408
409
|
const cur = resolveImage(cfg)
|
|
409
410
|
const dir = wallpaperImageDir()
|
|
411
|
+
/* v2.13.17:目录不存在时直接建出来,面板不再显示「?」,打开按钮也才有东西可开 */
|
|
412
|
+
try { mkdirSync(dir, { recursive: true }) } catch { /* 权限不足就保持原样 */ }
|
|
410
413
|
json(res, 200, {
|
|
411
414
|
ok: true,
|
|
412
415
|
dropDir: dir.replace(/\\/g, '/'),
|
|
@@ -464,7 +467,7 @@ export default {
|
|
|
464
467
|
* 只接受白名单键,值做长度/类型校验,避免面板把配置写成垃圾。
|
|
465
468
|
*/
|
|
466
469
|
const ALLOWED_SET = new Set(['image', 'imageFit', 'veilLightA', 'veilLightB', 'veilDarkA', 'veilDarkB', 'veilBlur', 'banner', 'bannerOpacity',
|
|
467
|
-
'bgMode', 'videoMode', 'videoId', 'videoShuffleMinutes'])
|
|
470
|
+
'bgMode', 'videoMode', 'videoId', 'videoShuffleMinutes', 'colorMode'])
|
|
468
471
|
const setHandler = (req, res) => {
|
|
469
472
|
let body = ''
|
|
470
473
|
req.on('data', (c) => { body += c; if (body.length > 16384) req.destroy() })
|
|
@@ -483,6 +486,7 @@ export default {
|
|
|
483
486
|
continue
|
|
484
487
|
}
|
|
485
488
|
if (k === 'bgMode') { if (v === 'image' || v === 'video') { cfg.bgMode = v; applied.bgMode = v } continue }
|
|
489
|
+
if (k === 'colorMode') { if (v === 'color' || v === 'mono') { cfg.colorMode = v; applied.colorMode = v } continue }
|
|
486
490
|
if (k === 'videoMode') { if (v === 'single' || v === 'shuffle') { cfg.videoMode = v; applied.videoMode = v } continue }
|
|
487
491
|
if (k === 'videoShuffleMinutes') {
|
|
488
492
|
const n = Number(v)
|
|
@@ -691,6 +695,8 @@ export default {
|
|
|
691
695
|
}
|
|
692
696
|
}
|
|
693
697
|
|
|
698
|
+
/* v2.13.17:开机就把丢图目录建好;老用户/新装用户都不会再看到「?」 */
|
|
699
|
+
try { mkdirSync(wallpaperImageDir(), { recursive: true }) } catch { /* 忽略 */ }
|
|
694
700
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/chat-wallpaper/config', handler: configHandler }))
|
|
695
701
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/chat-wallpaper/image', handler: imageHandler }))
|
|
696
702
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/chat-wallpaper/images', handler: imagesHandler }))
|