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.
Files changed (56) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +3 -1
  3. package/lib/assets/voices/f/line-1.mp3 +0 -0
  4. package/lib/assets/voices/f/line-2.mp3 +0 -0
  5. package/lib/assets/voices/f/line-3.mp3 +0 -0
  6. package/lib/assets/voices/f/line-4.mp3 +0 -0
  7. package/lib/assets/voices/f/line-5.mp3 +0 -0
  8. package/lib/assets/voices/f/line-6.mp3 +0 -0
  9. package/lib/assets/voices/g/line-1.mp3 +0 -0
  10. package/lib/assets/voices/g/line-2.mp3 +0 -0
  11. package/lib/assets/voices/g/line-3.mp3 +0 -0
  12. package/lib/assets/voices/g/line-4.mp3 +0 -0
  13. package/lib/assets/voices/g/line-5.mp3 +0 -0
  14. package/lib/assets/voices/g/line-6.mp3 +0 -0
  15. package/lib/assets/voices/h/line-1.mp3 +0 -0
  16. package/lib/assets/voices/h/line-2.mp3 +0 -0
  17. package/lib/assets/voices/h/line-3.mp3 +0 -0
  18. package/lib/assets/voices/h/line-4.mp3 +0 -0
  19. package/lib/assets/voices/h/line-5.mp3 +0 -0
  20. package/lib/assets/voices/h/line-6.mp3 +0 -0
  21. package/lib/assets/voices/i/line-1.mp3 +0 -0
  22. package/lib/assets/voices/i/line-2.mp3 +0 -0
  23. package/lib/assets/voices/i/line-3.mp3 +0 -0
  24. package/lib/assets/voices/i/line-4.mp3 +0 -0
  25. package/lib/assets/voices/i/line-5.mp3 +0 -0
  26. package/lib/assets/voices/i/line-6.mp3 +0 -0
  27. package/lib/assets/voices/j/line-1.mp3 +0 -0
  28. package/lib/assets/voices/j/line-2.mp3 +0 -0
  29. package/lib/assets/voices/j/line-3.mp3 +0 -0
  30. package/lib/assets/voices/j/line-4.mp3 +0 -0
  31. package/lib/assets/voices/j/line-5.mp3 +0 -0
  32. package/lib/assets/voices/j/line-6.mp3 +0 -0
  33. package/lib/assets/voices/k/line-1.mp3 +0 -0
  34. package/lib/assets/voices/k/line-2.mp3 +0 -0
  35. package/lib/assets/voices/k/line-3.mp3 +0 -0
  36. package/lib/assets/voices/k/line-4.mp3 +0 -0
  37. package/lib/assets/voices/k/line-5.mp3 +0 -0
  38. package/lib/assets/voices/k/line-6.mp3 +0 -0
  39. package/lib/assets/voices/manifest.json +32 -0
  40. package/lib/client.js +145 -64
  41. package/lib/host-startup.js +81 -4
  42. package/lib/host-wallpaper.js +8 -2
  43. package/lib/splash.js +73 -23
  44. package/package.json +2 -2
  45. package/test/smoke-client.mjs +27 -4
  46. package/test/smoke-host.mjs +29 -4
  47. package/test/smoke-package.mjs +4 -1
  48. package/test/smoke-splash.mjs +8 -1
  49. package/test/smoke-wallpaper-client.mjs +1 -1
  50. package/test/smoke-wallpaper-host.mjs +7 -2
  51. package/lib/assets/line-1.mp3 +0 -0
  52. package/lib/assets/line-2.mp3 +0 -0
  53. package/lib/assets/line-3.mp3 +0 -0
  54. package/lib/assets/line-4.mp3 +0 -0
  55. package/lib/assets/line-5.mp3 +0 -0
  56. package/lib/assets/line-6.mp3 +0 -0
package/lib/splash.js CHANGED
@@ -88,7 +88,8 @@
88
88
  voiceCue: true, // 念台词前的一声轻提示
89
89
  sfx: 'audio', // audio=真素材(用户音频里抠的音效)/ synth=现场合成
90
90
  voiceSource: 'asset', // asset=优先用真声素材 / tts=全部 TTS 合成
91
- voiceLead: 240, // 提示音之后留一小段空白再开口
91
+ voiceLead: 240,
92
+ welcomeDelay: 550, // 终幕欢迎语相对 05→06 和声钟的延迟(ms)
92
93
  identity: 'JOYCE MOORE', // ← 设置页可改的"身份名称"
93
94
  identityId: '0087',
94
95
  accessLevel: '3',
@@ -264,6 +265,9 @@
264
265
  没有就回落全局 lib/assets/line-N.mp3(原来的行为)。
265
266
  这样不同模板可以有各自的配音,互不覆盖。 */
266
267
  function assetUrl(name) {
268
+ /* 扩展音色包:slot 形如 voice:a:line-1,走 /dsh-startup/voice/a/line-1.mp3 */
269
+ var mv = /^voice:([a-z]):(line-[1-9]\d?)$/.exec(name)
270
+ if (mv) return '/dsh-startup/voice/' + mv[1] + '/' + mv[2] + '.mp3'
267
271
  var map = (TEMPLATE && TEMPLATE.assets) || null
268
272
  var file = (map && map[name]) ? String(map[name]).replace(/^.*\//, '') : ''
269
273
  if (file && /^[a-z0-9][a-z0-9._-]{0,40}\.(mp3|wav|ogg|m4a|aac)$/i.test(file) && TEMPLATE && TEMPLATE.id) {
@@ -1299,7 +1303,6 @@
1299
1303
  function ensureAsset(name) {
1300
1304
  if (buffers[name]) return Promise.resolve(true)
1301
1305
  if (pending[name]) return pending[name]
1302
- if (C.voiceSource === 'tts') return Promise.resolve(false)
1303
1306
  pending[name] = new Promise(function (resolve) {
1304
1307
  try {
1305
1308
  fetch(assetUrl(name))
@@ -1314,20 +1317,58 @@
1314
1317
  return pending[name]
1315
1318
  }
1316
1319
 
1317
- /** 播素材并等它播完,resolve 实际时长(ms)。拿不到素材 resolve 0。 */
1318
- function playAssetAndWait(name, gain) {
1320
+ /* 配音效果预设:与模板编辑器里的 5 个音效同参数(AI 机械音/机器人/无线电/老电视/低沉)。
1321
+ 素材先解码成 buffer,再在 BufferSource 后面串滤波 + 失真 + 环形调制,实时生效。 */
1322
+ var VOICE_FX_PRESETS = {
1323
+ 'ai-mech': { hp: 130, lp: 4200, peak: { f: 260, g: 4, q: 1.0 }, drive: 0.15, ring: 0 },
1324
+ 'robot': { hp: 180, lp: 3200, peak: { f: 1500, g: 3, q: 0.9 }, drive: 0.35, ring: 42 },
1325
+ 'radio': { hp: 420, lp: 2800, peak: { f: 1800, g: 5, q: 1.2 }, drive: 0.6, ring: 0 },
1326
+ 'oldtv': { hp: 220, lp: 2200, peak: null, drive: 0.45, ring: 0 },
1327
+ 'dark': { hp: 90, lp: 1200, peak: { f: 180, g: 5, q: 0.8 }, drive: 0.1, ring: 0 }
1328
+ }
1329
+ function voiceDriveCurve(amount) {
1330
+ var k = Math.max(0, Math.min(1, Number(amount) || 0)) * 60
1331
+ var n = 1024, curve = new Float32Array(n)
1332
+ for (var i = 0; i < n; i++) {
1333
+ var x = (i * 2) / n - 1
1334
+ curve[i] = ((3 + k) * x * 20 * Math.PI / 180) / (Math.PI + k * Math.abs(x))
1335
+ }
1336
+ return curve
1337
+ }
1338
+ /** 播素材并等它播完,resolve 实际时长(ms)。fx 传 5 个效果之一;拿不到素材 resolve 0。 */
1339
+ function playAssetAndWait(name, gain, fx) {
1319
1340
  return new Promise(function (resolve) {
1320
1341
  var c = ac(true)
1321
1342
  var buf = buffers[name]
1322
1343
  if (!c || !buf) { resolve(0); return }
1344
+ var osc = null
1323
1345
  try {
1324
1346
  var src = c.createBufferSource()
1325
1347
  src.buffer = buf
1326
1348
  var g = c.createGain()
1327
1349
  g.gain.value = Math.min(1, (gain == null ? 0.85 : gain) * clamp01(C.voiceVolume, 0, 1) * 1.6)
1328
- src.connect(g); g.connect(c.destination)
1329
- src.onended = function () { resolve(Math.round(buf.duration * 1000)) }
1350
+ var node = src
1351
+ var P = fx && VOICE_FX_PRESETS[fx]
1352
+ if (P) {
1353
+ if (P.hp) { var hp = c.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = P.hp; node.connect(hp); node = hp }
1354
+ if (P.lp) { var lp = c.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = P.lp; node.connect(lp); node = lp }
1355
+ if (P.peak) { var pk = c.createBiquadFilter(); pk.type = 'peaking'; pk.frequency.value = P.peak.f; pk.gain.value = P.peak.g; pk.Q.value = P.peak.q; node.connect(pk); node = pk }
1356
+ if (P.drive) { var ws = c.createWaveShaper(); ws.curve = voiceDriveCurve(P.drive); ws.oversample = '2x'; node.connect(ws); node = ws }
1357
+ if (P.ring) {
1358
+ var ringGain = c.createGain(); ringGain.gain.value = 0.55
1359
+ osc = c.createOscillator(); osc.frequency.value = P.ring; osc.type = 'sine'
1360
+ var modGain = c.createGain(); modGain.gain.value = 0.45
1361
+ osc.connect(modGain); modGain.connect(ringGain.gain)
1362
+ node.connect(ringGain); node = ringGain
1363
+ }
1364
+ }
1365
+ node.connect(g); g.connect(c.destination)
1366
+ src.onended = function () {
1367
+ if (osc) { try { osc.stop() } catch (eO) {}; try { osc.disconnect() } catch (eD) {} }
1368
+ resolve(Math.round(buf.duration * 1000))
1369
+ }
1330
1370
  src.start()
1371
+ if (osc) { try { osc.start() } catch (eS) {} }
1331
1372
  } catch (e) { resolve(0) }
1332
1373
  })
1333
1374
  }
@@ -1599,28 +1640,32 @@
1599
1640
  * onend 时按估算兜底,绝不把动画卡死。
1600
1641
  */
1601
1642
  line: function (en, zh, asset) {
1602
- /* 有真声素材就优先用素材(音色就是你给的那个);拿不到再回落 TTS */
1603
- if (asset && C.voice && C.voiceSource !== 'tts' && !finished) {
1604
- return audio.ensureAsset(asset).then(function (ok) {
1605
- if (!ok || finished) return 0
1606
- if (C.voiceCue !== false) audio.cue()
1607
- return audio.playAssetAndWait(asset, 0.85).then(function (ms) {
1608
- if (ms > 0) setMeter(true)
1609
- return ms
1610
- })
1611
- }).then(function (ms) {
1612
- if (ms > 0) { saidOnce = true; return ms }
1613
- return ttsLine(en, zh)
1614
- })
1643
+ /* v2.13.16:旧 TTS 配音链路退役。这里只用「音色包 + 声音效果」两段:
1644
+ 音色包决定用哪套干音(a–k);效果在运行时叠加。
1645
+ 素材缺失时静音,不再回落系统 TTS,避免旧机械音盖掉新音色。 */
1646
+ if (!asset || !C.voice || finished) return Promise.resolve(0)
1647
+ var useAsset = asset
1648
+ var tplAssets = (TEMPLATE && TEMPLATE.assets) || null
1649
+ var isGlobalLine = /^line-[1-9]\d?$/.test(asset) && !(tplAssets && tplAssets[asset])
1650
+ if (isGlobalLine) {
1651
+ var pack = String(C.voicePack || 'a')
1652
+ if (/^[a-k]$/.test(pack)) useAsset = 'voice:' + pack + ':' + asset
1615
1653
  }
1616
- return ttsLine(en, zh)
1654
+ var fx = String(C.voiceFx || 'none')
1655
+ return audio.ensureAsset(useAsset).then(function (ok) {
1656
+ if (!ok || finished) return 0
1657
+ if (C.voiceCue !== false) audio.cue()
1658
+ return audio.playAssetAndWait(useAsset, 0.85, isGlobalLine ? fx : 'none').then(function (ms) {
1659
+ if (ms > 0) setMeter(true)
1660
+ return ms
1661
+ })
1662
+ })
1617
1663
  },
1618
-
1619
1664
  /** TTS 路径:素材缺失 / voiceSource=tts / 终幕那条动态名字 */
1620
1665
  tts: function (en, zh) { return ttsLine(en, zh) },
1621
1666
 
1622
1667
  /** 不等待的版本:设置页试听 / 任何"念完就行"的场合 */
1623
- say: function (en, zh) { return this.line(en, zh) },
1668
+ say: function (en, zh) { return ttsLine(en, zh) },
1624
1669
 
1625
1670
  /** 浏览器要求"先有用户手势"才放声:第一次点击/按键解锁 + 补播当前行 */
1626
1671
  unlock: function () {
@@ -3455,7 +3500,12 @@
3455
3500
  /* 终幕欢迎语:用从 Video Project 3 截出来的真声素材(line-6)。
3456
3501
  素材缺失 / voiceSource=tts / voice 关闭时自动静音,不会退回 TTS 念白 ——
3457
3502
  因为这条素材是「欢迎访问」整句,和动态身份名称无关。 */
3458
- var welcome = voice.line(null, null, 'line-6')
3503
+ /* 05→06 的过场和声钟约 1.5 秒。旧行为是钟声一响欢迎语就开口,听感太挤;
3504
+ 现在按配置等钟声收尾再进最后一句,默认 1500ms,可在设置里微调。 */
3505
+ var welcomeDelay = Number(C.welcomeDelay)
3506
+ if (!isFinite(welcomeDelay)) welcomeDelay = 1500
3507
+ welcomeDelay = Math.max(0, Math.min(3000, welcomeDelay))
3508
+ var welcome = wait(welcomeDelay).then(function () { return voice.line(null, null, 'line-6') })
3459
3509
 
3460
3510
  ui.status.classList.remove('dsu-in')
3461
3511
  ui.subEn.classList.remove('dsu-in')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-boot-suite",
3
- "version": "2.13.11",
4
- "description": "DeepSeek Harness 开机套件:冷启动开机动画 + 聊天壁纸。开机动画走设置页「启动动画」(两幕可选,默认档案暖白 → 鲸鱼娘扫描睁眼;支持导入自定义动画、三段自定义转场、本地动画删除、真声台词 + 界面音效);聊天壁纸走设置页「聊天背景」(自带一张默认背景图,开箱即有;静态底图(可铺满或完整显示)/ 循环视频 + 大字横幅,聊天正文文字随壁纸+纱的实际亮度自适应(纱浓度可调),侧边栏/标题栏跟随外观深浅;桌面窗口按钮颜色固定高对比,支持丢图即换、自定义视频目录、壁纸引擎项目文件夹、纱的浓度与镜头模糊调节,视频默认静音循环)。两个半侧各自独立:动画坏了不影响壁纸,壁纸坏了不影响动画。",
3
+ "version": "2.13.19",
4
+ "description": "DeepSeek Harness 启动动画与聊天背景插件:两幕开机动画、自定义动画导入、三段转场、6 套干音音色(英文 1 + 日文 5)+ 5 种声音效果、聊天背景图片/循环视频、更新前自动回滚;聊天背景新增黑白原版素材模式。",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -112,6 +112,12 @@ const fetchStub = (url, init) => {
112
112
  if (!HIDDEN.includes(id)) HIDDEN.push(id)
113
113
  return Promise.resolve({ json: () => Promise.resolve({ ok: true, id, removed: false, hidden: true }) })
114
114
  }
115
+ if (u.includes('/dsh-startup/voices')) {
116
+ return Promise.resolve({ json: () => Promise.resolve({ ok: true, packs: [
117
+ { id: 'f', name: '英文音色F(默认)', lang: 'en' },
118
+ { id: 'g', name: '日文音色1', lang: 'ja' },
119
+ ], default: 'f' }) })
120
+ }
115
121
  if (u.includes('/dsh-startup/templates')) {
116
122
  return Promise.resolve({ json: () => Promise.resolve({ ok: true, templates: liveTemplates(), hidden: HIDDEN.slice() }) })
117
123
  }
@@ -211,10 +217,13 @@ const idInput = find(tree, (n) => n.type === 'input' && n.props.type === 'text'
211
217
  check('身份名称输入框回填了当前配置', !!idInput, idInput && idInput.props.value)
212
218
  check('渲染出「启动动画」开关', texts.includes('启用启动动画'))
213
219
  check('渲染出「女声播报」开关', texts.includes('女声播报(念每一行的英文)'))
214
- check('渲染出「播报语言」字段', texts.includes('播报语言'))
220
+ check('已删除旧 TTS「播报语言」字段', !texts.includes('播报语言'))
221
+ check('渲染出「配音音色」字段', texts.includes('配音音色'))
222
+ check('渲染出「声音效果」字段', texts.includes('声音效果'))
223
+ check('渲染出「终幕语音延迟」字段', texts.includes('终幕语音延迟'))
215
224
  check('渲染出「界面音效」开关', texts.includes('界面音效'))
216
- check('渲染出音色选择(无 speechSynthesis 时给出提示)', texts.includes('音色'))
217
- check('渲染出语速/音调/音量', texts.includes('语速') && texts.includes('音调') && texts.includes('音量'))
225
+ check('已删除旧 TTS 音色选择', !texts.includes('试听所选音色') && !texts.includes('语音来源'))
226
+ check('渲染出音量、不再有语速/音调', texts.includes('音量') && !texts.includes('语速') && !texts.includes('音调'))
218
227
  check('渲染出「立即预览」按钮', !!find(tree, (n) => n.type === 'button' && n.props.children === '立即预览'))
219
228
  check('渲染出「恢复默认」按钮', !!find(tree, (n) => n.type === 'button' && n.props.children === '恢复默认'))
220
229
 
@@ -490,7 +499,21 @@ const beforePreview = requests.length
490
499
  previewBtn.props.onClick()
491
500
  await flush(); await flush(); await flush()
492
501
  check('立即预览先重新拉配置', requests.length > beforePreview && requests.slice(beforePreview).some((r) => /\/dsh-startup\/config/.test(r.url || '')), '')
493
- check('立即预览调用 __DSH_STARTUP_API__.replay()', windowStub.__replayed === true)
502
+ check('立即预览调用 __DSH_STARTUP_API__.replay()', windowStub.__replayed === true)
503
+ /* v2.13.15 回归:选完「配音音色」直接点预览(还没点保存)也必须按当前选择播 */
504
+ tree = renderPanel()
505
+ const voiceSel = walk(tree).filter((n) => n.type === 'select' && Array.isArray(n.props.children) && n.props.children.some((o) => o && o.props.value === 'g'))[0]
506
+ check('配音音色下拉能从 /voices 拿到音色G', !!voiceSel, JSON.stringify(walk(tree).filter((n) => n.type === 'select').map((n) => (n.props.children || []).map((o) => o && o.props.value))))
507
+ if (voiceSel) {
508
+ voiceSel.props.onChange({ target: { value: 'g' } })
509
+ await flush()
510
+ tree = renderPanel()
511
+ const previewBtn2 = find(tree, (n) => n.type === 'button' && n.props.children === '立即预览')
512
+ previewBtn2.props.onClick()
513
+ await flush(); await flush(); await flush()
514
+ check('立即预览使用未保存的 draft(voicePack=g)', windowStub.__DSH_STARTUP__ && windowStub.__DSH_STARTUP__.voicePack === 'g', windowStub.__DSH_STARTUP__ && windowStub.__DSH_STARTUP__.voicePack)
515
+ check('preview 合并 draft 的源码标记存在', code.includes('j.config, draft || {}'))
516
+ }
494
517
  delete windowStub.__DSH_STARTUP_API__
495
518
  previewBtn.props.onClick()
496
519
  await flush(); await flush(); await flush()
@@ -132,7 +132,7 @@ check('没有 Config 导出(普通对象不是 schemastery schema)', mod.Con
132
132
 
133
133
  console.log('\n[2] apply(ctx):路由与注入')
134
134
  mod.apply(ctx)
135
- check('注册了 37 条路由(开机动画 26 + 聊天壁纸 11)', ROUTES.size === 37, [...ROUTES.keys()].join(' | '))
135
+ check('注册了 40 条路由(开机动画 29 + 聊天壁纸 11)', ROUTES.size === 40, [...ROUTES.keys()].join(' | '))
136
136
  check('注册了 1 个 tapIndex', TAPS.length === 1)
137
137
  check('用了 ctx.effect 管理生命周期(开机动画 1 + 壁纸 11)', effects === 12, String(effects))
138
138
 
@@ -154,9 +154,9 @@ r = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asse
154
154
  check('目录穿越被拒 -> 404', r.statusCode === 404, String(r.statusCode))
155
155
  r = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asset/nope.mp3')
156
156
  check('不存在的素材 -> 404', r.statusCode === 404, String(r.statusCode))
157
- for (const f of ['line-1', 'line-2', 'line-3', 'line-4', 'line-5']) {
158
- const rr = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asset/' + f + '.mp3')
159
- check('真声素材 ' + f + '.mp3 -> 200', rr.statusCode === 200 && rr.body.length > 5000, rr.statusCode + ' ' + rr.body.length)
157
+ for (const f of ['line-1', 'dry-line-1']) {
158
+ const rr = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asset/' + f + '.mp3')
159
+ check('旧 TTS 素材已退役:' + f + '.mp3 -> 404', rr.statusCode === 404, String(rr.statusCode))
160
160
  }
161
161
 
162
162
  console.log('\n[4] 配置读写落盘')
@@ -165,6 +165,9 @@ let cfg = JSON.parse(r.body)
165
165
  check('GET config ok', cfg.ok === true)
166
166
  check('默认身份名称 = JOYCE MOORE', cfg.config.identity === 'JOYCE MOORE', cfg.config.identity)
167
167
  check('默认身份编号 = 0087', cfg.config.identityId === '0087', cfg.config.identityId)
168
+ check('默认配音音色 = F(英文)', cfg.config.voicePack === 'f', cfg.config.voicePack)
169
+ check('默认声音效果 = 机器人(robot)', cfg.config.voiceFx === 'robot', cfg.config.voiceFx)
170
+ check('默认终幕语音延迟 = 550ms', cfg.config.welcomeDelay === 550, cfg.config.welcomeDelay)
168
171
  check('返回了配置文件路径', typeof cfg.file === 'string' && cfg.file.startsWith(SMOKE_HOME), cfg.file)
169
172
  check('返回了插件版本号(设置面板顶部要显示它)', typeof cfg.version === 'string' && /^\d+\.\d+\.\d+/.test(cfg.version), cfg.version)
170
173
 
@@ -427,6 +430,28 @@ console.log('\n[6g] 更新包完整性:只看 PK 头的半包不再被接受')
427
430
  check('坏包不落盘', !fs.existsSync(path.join(SMOKE_HOME, 'boot-suite-updates', '9.9.9.zip')))
428
431
  }
429
432
 
433
+ console.log('\n[6h] v2.13.12:打开下载文件夹路由存在且只接受 POST')
434
+ {
435
+ const getOpen = await call('exact /dsh-startup/open-downloads', 'GET')
436
+ check('GET /open-downloads 返回 405', getOpen.statusCode === 405, String(getOpen.statusCode))
437
+ }
438
+
439
+ console.log('\n[6i] v2.13.19:6 套音色包清单与只读音频路由')
440
+ {
441
+ const vp = await call('exact /dsh-startup/voices', 'GET')
442
+ const vpj = JSON.parse(vp.body)
443
+ check('GET /voices ok', vpj.ok === true)
444
+ check('6 个音色包(f、g–k)', Array.isArray(vpj.packs) && vpj.packs.length === 6, String(vpj.packs && vpj.packs.length))
445
+ check('F 包英文 / K 包日文', vpj.packs[0].id === 'f' && vpj.packs[0].lang === 'en' && vpj.packs[5].id === 'k' && vpj.packs[5].lang === 'ja', JSON.stringify(vpj.packs.slice(0, 1).concat(vpj.packs.slice(-1))))
446
+ check('默认音色 = f', vpj.default === 'f', String(vpj.default))
447
+ const va = await call('prefix /dsh-startup/voice', 'GET', undefined, '/dsh-startup/voice/f/line-1.mp3')
448
+ check('音色F line-1 音频 200 且是 mp3', va.statusCode === 200 && va.body.length > 1000 && (va.body[0] === 0xFF || va.body.slice(0, 3).toString() === 'ID3'), va.statusCode + ' ' + va.body.length)
449
+ const vb = await call('prefix /dsh-startup/voice', 'GET', undefined, '/dsh-startup/voice/f/line-7.mp3')
450
+ check('非法句号 line-7 -> 404', vb.statusCode === 404, String(vb.statusCode))
451
+ const vz = await call('prefix /dsh-startup/voice', 'GET', undefined, '/dsh-startup/voice/z/line-1.mp3')
452
+ check('未注册音色 z -> 404', vz.statusCode === 404, String(vz.statusCode))
453
+ }
454
+
430
455
  console.log('\n[7] XSS 边界:身份名称里的尖括号不会逃出内联脚本')
431
456
  await call('exact /dsh-startup/config', 'POST', JSON.stringify({ patch: { identity: '</script><img src=x onerror=alert(1)>' } }))
432
457
  const payload = inlinePayload(renderRows(before, collectRows()))
@@ -8,7 +8,7 @@ if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
8
8
  }
9
9
  const pkg = JSON.parse(buf.toString('utf8'));
10
10
  if (!pkg.version) throw new Error('package.json 缺少 version');
11
- for (const f of ['lib/index.js', 'lib/splash.js', 'lib/client.js', 'lib/assets/default-background.jpg', 'templates', 'tools/install.ps1', 'tools/update-apply.ps1', 'tools/rollback-version.ps1']) {
11
+ for (const f of ['lib/index.js', 'lib/splash.js', 'lib/client.js', 'lib/assets/default-background.jpg', 'templates', 'tools/install.ps1', 'tools/update-apply.ps1', 'tools/rollback-version.ps1', 'lib/assets/voices/manifest.json']) {
12
12
  if (!fs.existsSync(path.join(root, f))) throw new Error('缺少 ' + f);
13
13
  }
14
14
  const installPs1 = fs.readFileSync(path.join(root, 'tools/install.ps1'), 'utf8');
@@ -24,4 +24,7 @@ const rollbackPs1 = fs.readFileSync(path.join(root, 'tools/rollback-version.ps1'
24
24
  for (const needle of ['TargetPkg', 'CheckOnly', 'DSH_BOOT_SUITE_ROLLBACK', '回滚日志']) {
25
25
  if (!rollbackPs1.includes(needle)) throw new Error('rollback-version.ps1 缺少关键逻辑:' + needle);
26
26
  }
27
+ for (const f of ['line-1.mp3', 'line-6.mp3', 'dry-line-1.mp3']) {
28
+ if (fs.existsSync(path.join(root, 'lib/assets', f))) throw new Error('旧 TTS 配音素材未删除:' + f);
29
+ }
27
30
  console.log('ok package.json version=' + pkg.version + ',无 BOM,关键文件齐全(含更新前回滚 checkpoint)');
@@ -327,11 +327,18 @@ console.log('\n[8] 终幕用真声欢迎语(来自 Video Project 3 的 line-6
327
327
  {
328
328
  const src = fs.readFileSync(BUNDLE, 'utf8')
329
329
  check('全程没有 voice.tts( 调用(不再用 TTS 念白)', !/voice\.tts\(/.test(src))
330
- check('终幕挂了 line-6 素材', /voice\.line\(null, null, 'line-6'\)/.test(src))
330
+ check('终幕挂了 line-6 素材', /voice\.line\(null, null, 'line-6'\)/.test(src))
331
+ check('终幕欢迎语按 welcomeDelay 延迟后播放', /C\.welcomeDelay/.test(src) && /welcomeDelay = Math\.max\(0, Math\.min\(3000/.test(src))
331
332
  check('揭幕前会等欢迎语播完', /\.then\(function \(\) \{ return welcome \}\)/.test(src))
332
333
  check('预加载包含 line-6', /ensureAsset\('line-6'\)/.test(src))
333
334
  check('五步仍然各自挂着真声素材', (src.match(/audio: 'line-/g) || []).length === 5)
334
335
  check('总共 6 份人声素材被引用', (src.match(/'line-[1-6]'/g) || []).length >= 6)
336
+ check('配音音色可切换:voicePack 为 f–k 时走 voice:<pack>:line-N 路由',
337
+ /C\.voicePack/.test(src) && /'voice:' \+ pack \+ ':' \+ asset/.test(src)
338
+ && /tplAssets && tplAssets\[asset\]/.test(src))
339
+ check('5 套声音效果预设都在,播放链路按 C.voiceFx 叠加',
340
+ ['ai-mech', 'robot', 'radio', 'oldtv', 'dark'].every((k) => src.includes("'" + k + "':"))
341
+ && /playAssetAndWait\(useAsset, 0\.85, isGlobalLine \? fx : 'none'\)/.test(src))
335
342
  }
336
343
 
337
344
  console.log('\n[9] 模板:分幕可以自定义(1~24 幕,缺字段回落内置)')
@@ -463,7 +463,7 @@ ok(intervals.every((t) => t.cleared) || intervals.length === 0, '旧的 interval
463
463
  intervals.map((t) => (t.cleared ? 'cleared' : 'live')).join(','))
464
464
 
465
465
  console.log('\n=== ⑦ v2.13.0 测试钩子 / BUILD ===')
466
- ok(mod.__BUILD === 'cw-34', '__BUILD = cw-34(排查浏览器跑的是不是新包)', String(mod.__BUILD))
466
+ ok(mod.__BUILD === 'cw-42', '__BUILD = cw-42(排查浏览器跑的是不是新包)', String(mod.__BUILD))
467
467
  ok(mod.__VIDEO_ID === 'dsh-cw-video', '__VIDEO_ID = dsh-cw-video', String(mod.__VIDEO_ID))
468
468
  ok(typeof mod.__startShuffle === 'function', '__startShuffle 导出')
469
469
  ok(typeof mod.__stopShuffle === 'function', '__stopShuffle 导出')
@@ -163,10 +163,10 @@ try {
163
163
  sortKeys(r.json) === 'config,image,imageOk,imageSource,ok,video', sortKeys(r.json))
164
164
  check('/config ok=true', r.json && r.json.ok === true, r.json && String(r.json.ok))
165
165
  const CONFIG_KEYS = ['banner', 'bannerOpacity', 'fallbackDark', 'fallbackLight', 'image', 'imageFit',
166
- 'sidebarDark', 'sidebarLight', 'veilBlur', 'veilDarkA', 'veilDarkB', 'veilLightA', 'veilLightB',
166
+ 'sidebarDark', 'sidebarLight', 'veilBlur', 'veilDarkA', 'veilDarkB', 'veilLightA', 'veilLightB', 'colorMode',
167
167
  /* v2.13.0 循环视频 */
168
168
  'bgMode', 'videoMode', 'videoId', 'videoRoots', 'videoShuffleMinutes']
169
- check('config 恰好 18 个键(12 旧 + 5 循环视频 + 1 镜头模糊)',
169
+ check('config 恰好 19 个键(12 旧 + 5 循环视频 + 1 镜头模糊 + 1 色彩模式)',
170
170
  sortKeys(r.json && r.json.config) === CONFIG_KEYS.slice().sort().join(','), sortKeys(r.json && r.json.config))
171
171
  const DEAD_KEYS = ['video', 'mode', 'placement', 'loopStart', 'loopEnd', 'oncePerSession',
172
172
  'waitForBoot', 'fadeOutMs', 'objectFit', 'opacity', 'playbackRate', 'fps']
@@ -177,6 +177,7 @@ try {
177
177
  check('/config 的 imageSource = builtin', r.json && r.json.imageSource === 'builtin', String(r.json && r.json.imageSource))
178
178
  check('★ v2.13.4 config.imageFit 默认 cover', r.json && r.json.config && r.json.config.imageFit === 'cover', String(r.json && r.json.config && r.json.config.imageFit))
179
179
  check('★ v2.13.11 config.veilBlur 默认 0(不模糊)', r.json && r.json.config && r.json.config.veilBlur === 0, String(r.json && r.json.config && r.json.config.veilBlur))
180
+ check('★ v2.13.17 config.colorMode 默认 color', r.json && r.json.config && r.json.config.colorMode === 'color', String(r.json && r.json.config && r.json.config.colorMode))
180
181
  const v0 = (r.json && r.json.video) || {}
181
182
  check('★ /config.video 默认值:enabled=false / mode=single / shuffleMinutes=0 / selectedId 空 / count=0 / current=null',
182
183
  v0.enabled === false && v0.mode === 'single' && v0.shuffleMinutes === 0 &&
@@ -228,6 +229,10 @@ try {
228
229
  check('★ veilBlur 下限夹到 0', r.json && r.json.applied && r.json.applied.veilBlur === 0, JSON.stringify(r.json && r.json.applied))
229
230
  r = await postJson('exact /chat-wallpaper/set', { veilBlur: 'abc' })
230
231
  check('★ 非法 veilBlur 被忽略', r.json && r.json.applied && !('veilBlur' in r.json.applied), JSON.stringify(r.json && r.json.applied))
232
+ r = await postJson('exact /chat-wallpaper/set', { colorMode: 'mono' })
233
+ check('★ v2.13.17 /set 接受 colorMode=mono', r.json && r.json.applied && r.json.applied.colorMode === 'mono', JSON.stringify(r.json && r.json.applied))
234
+ r = await postJson('exact /chat-wallpaper/set', { colorMode: 'rainbow' })
235
+ check('★ 非法 colorMode 被忽略', r.json && r.json.applied && !('colorMode' in r.json.applied), JSON.stringify(r.json && r.json.applied))
231
236
  r = await callJson('exact /chat-wallpaper/config')
232
237
  check('再读 /config:banner = B', r.json && r.json.config && r.json.config.banner === 'B', r.json && r.json.config && r.json.config.banner)
233
238
  const cfgFile = path.join(HOME, 'chat-wallpaper.json')
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file