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/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
|
-
|
|
1318
|
-
|
|
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
|
|
1329
|
-
|
|
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
|
-
/*
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|
package/test/smoke-client.mjs
CHANGED
|
@@ -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('
|
|
220
|
+
check('已删除旧 TTS「播报语言」字段', !texts.includes('播报语言'))
|
|
221
|
+
check('渲染出「配音音色」字段', texts.includes('配音音色'))
|
|
222
|
+
check('渲染出「声音效果」字段', texts.includes('声音效果'))
|
|
223
|
+
check('渲染出「终幕语音延迟」字段', texts.includes('终幕语音延迟'))
|
|
215
224
|
check('渲染出「界面音效」开关', texts.includes('界面音效'))
|
|
216
|
-
check('
|
|
217
|
-
check('
|
|
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()
|
package/test/smoke-host.mjs
CHANGED
|
@@ -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('注册了
|
|
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', '
|
|
158
|
-
const rr = await call('prefix /dsh-startup/asset', 'GET', undefined, '/dsh-startup/asset/' + f + '.mp3')
|
|
159
|
-
check('
|
|
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()))
|
package/test/smoke-package.mjs
CHANGED
|
@@ -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)');
|
package/test/smoke-splash.mjs
CHANGED
|
@@ -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-
|
|
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 恰好
|
|
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')
|
package/lib/assets/line-1.mp3
DELETED
|
Binary file
|
package/lib/assets/line-2.mp3
DELETED
|
Binary file
|
package/lib/assets/line-3.mp3
DELETED
|
Binary file
|
package/lib/assets/line-4.mp3
DELETED
|
Binary file
|
package/lib/assets/line-5.mp3
DELETED
|
Binary file
|
package/lib/assets/line-6.mp3
DELETED
|
Binary file
|