dsh-notify-say 0.4.54 → 0.4.55
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 +1 -1
- package/lib/client.js +150 -8
- package/package.json +1 -1
- package/test/voice-label.test.mjs +123 -0
package/README.md
CHANGED
|
@@ -330,7 +330,7 @@ npm run test:route # 需要 --import loader 的接线测试
|
|
|
330
330
|
npm run test:audio-wire
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
-
**
|
|
333
|
+
**414 条测试通过。** 绝大多数是纯逻辑测试,克隆仓库后 `npm test` 即可跑。
|
|
334
334
|
|
|
335
335
|
少数用例需要真实的 DSH 宿主(用于验证 Cordis 上下文行为)。
|
|
336
336
|
找不到宿主时它们会**自动 skip** —— 注意 skip 不等于通过。
|
package/lib/client.js
CHANGED
|
@@ -266,6 +266,56 @@ window.__ModuleLoader__.load({
|
|
|
266
266
|
opacity: 0.78, fontSize: '12.5px',
|
|
267
267
|
},
|
|
268
268
|
|
|
269
|
+
// ── 试听按钮组(v0.4.55)────────────────────────────────────────
|
|
270
|
+
//
|
|
271
|
+
// 用户反馈:420px 下五个按钮挤成 4+1,最后一个「拦截」孤零零一行。
|
|
272
|
+
//
|
|
273
|
+
// 改法:整组**独占一行**(flex-basis 100%),用 grid 自动定列数。
|
|
274
|
+
// · 420px:内容宽约 364 → 排得下 5 列(每列约 63px)→ 一行五个
|
|
275
|
+
// · 更窄时自动降列 → 自然换行,行距列距一致(都是 6px)
|
|
276
|
+
// · maxWidth 340:720px 下不让按钮被拉成大块头
|
|
277
|
+
//
|
|
278
|
+
// 不缩小按钮:minmax 下限 56px 比原来的约 51px 还宽一点。
|
|
279
|
+
pvGroup: {
|
|
280
|
+
display: 'grid',
|
|
281
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(56px, 1fr))',
|
|
282
|
+
gap: '6px',
|
|
283
|
+
flex: '1 1 100%',
|
|
284
|
+
maxWidth: '340px',
|
|
285
|
+
marginTop: '2px',
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// ── 中性提示框(v0.4.55)────────────────────────────────────────
|
|
289
|
+
//
|
|
290
|
+
// 用于「不是错误、但用户应当知道」的状态(如弹窗与声音都关掉)。
|
|
291
|
+
// 刻意**不用红/黄**:这不是故障,是用户自己的选择,只需告知结果。
|
|
292
|
+
noticeBox: {
|
|
293
|
+
margin: '8px 0 2px',
|
|
294
|
+
padding: '8px 10px',
|
|
295
|
+
borderLeft: '3px solid rgba(128,128,128,.5)',
|
|
296
|
+
background: 'rgba(128,128,128,.08)',
|
|
297
|
+
fontSize: '12.5px',
|
|
298
|
+
lineHeight: '1.6',
|
|
299
|
+
opacity: 0.92,
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// 可点击的折叠标题(v0.4.55):整行可点,箭头固定在最右。
|
|
303
|
+
foldHead: {
|
|
304
|
+
display: 'flex',
|
|
305
|
+
alignItems: 'center',
|
|
306
|
+
justifyContent: 'space-between',
|
|
307
|
+
gap: '8px',
|
|
308
|
+
cursor: 'pointer',
|
|
309
|
+
userSelect: 'none',
|
|
310
|
+
margin: '16px 0 2px',
|
|
311
|
+
padding: '2px 4px',
|
|
312
|
+
marginLeft: '-4px',
|
|
313
|
+
borderRadius: '4px',
|
|
314
|
+
},
|
|
315
|
+
// 悬停/键盘焦点时的反馈(同一套,键盘用户也要看得见)。
|
|
316
|
+
foldHeadActive: { background: 'rgba(128,128,128,.14)' },
|
|
317
|
+
foldArrow: { flex: '0 0 auto', opacity: 0.6, fontSize: '11px' },
|
|
318
|
+
|
|
269
319
|
// 说明文字:弱化但保持可读对比度(不要低到看不清)。
|
|
270
320
|
fieldHint: { fontSize: '12.5px', opacity: 0.72, lineHeight: '1.6', flex: '1 1 100%', margin: '0 0 7px' },
|
|
271
321
|
|
|
@@ -416,6 +466,54 @@ window.__ModuleLoader__.load({
|
|
|
416
466
|
'shikoku-metang', 'ameharu-hau', 'namiine-ritsu', 'shirakami-kotaro',
|
|
417
467
|
]
|
|
418
468
|
|
|
469
|
+
/**
|
|
470
|
+
* 音色友好显示名(v0.4.55)。
|
|
471
|
+
*
|
|
472
|
+
* 用户反馈:下拉里显示 `zundamon-normal` 是**开发者内部标识**,
|
|
473
|
+
* 普通用户看不懂。改为「角色名 · 风格名」;原 key 只在 title 悬停提示
|
|
474
|
+
* 与诊断日志里出现。
|
|
475
|
+
*
|
|
476
|
+
* ⚠️ 只影响**显示**:保存提交的仍是原 key(voiceName / kindVoiceName),
|
|
477
|
+
* 配置格式、默认值、后端一字未动。
|
|
478
|
+
*
|
|
479
|
+
* ⚠️ 名字与 `tools/build-voice-packs.ps1` 的 $Approved.Name 同源,
|
|
480
|
+
* 由测试 cross-check 防漂移(见 test/voice-label.test.mjs)。
|
|
481
|
+
*/
|
|
482
|
+
var VOICE_LABELS = {
|
|
483
|
+
'zundamon-normal': 'ずんだもん · ノーマル',
|
|
484
|
+
'zundamon-amama': 'ずんだもん · あまあま',
|
|
485
|
+
'ankomon': 'あんこもん',
|
|
486
|
+
'goki-nuigurumi': '後鬼 · ぬいぐるみ',
|
|
487
|
+
'kyushu-sora': '九州そら',
|
|
488
|
+
'manbetsu-hanamaru-genki': '満別花丸 · 元気',
|
|
489
|
+
'meimei-meria': '冥鳴ひまり',
|
|
490
|
+
'shikoku-metang': '四国めたん',
|
|
491
|
+
'ameharu-hau': '雨晴はう',
|
|
492
|
+
'namiine-ritsu': '波音リツ',
|
|
493
|
+
'shirakami-kotaro': '白上虎太郎',
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* key → 友好名。未知 key **原样返回**(不猜、不美化)——
|
|
498
|
+
* 用户手填的自定义 key 显示成它自己,比显示一个错的别名安全。
|
|
499
|
+
*/
|
|
500
|
+
function voiceLabel(key) {
|
|
501
|
+
if (!key) return ''
|
|
502
|
+
return VOICE_LABELS[key] || key
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 语音包元数据里的官方名(`ずんだもん(ノーマル)`)
|
|
507
|
+
* → 统一成「角色名 · 风格名」,与 VOICE_LABELS 风格一致。
|
|
508
|
+
*
|
|
509
|
+
* 否则「语音包列表」用括号、「音色下拉」用点,同一件事两种叫法。
|
|
510
|
+
*/
|
|
511
|
+
function prettyPackName(name) {
|
|
512
|
+
var s = String(name || '')
|
|
513
|
+
var m = /^(.+?)((.+?))$/.exec(s)
|
|
514
|
+
return m ? (m[1] + ' · ' + m[2]) : s
|
|
515
|
+
}
|
|
516
|
+
|
|
419
517
|
/** 事件开关定义(键 → 中文名)。 */
|
|
420
518
|
// 事件开关:[键, 名称, 一句话说明]。
|
|
421
519
|
//
|
|
@@ -493,6 +591,13 @@ window.__ModuleLoader__.load({
|
|
|
493
591
|
var avset = avst[1]
|
|
494
592
|
var advSec = avst[0]
|
|
495
593
|
|
|
594
|
+
// 高级子组标题的悬停/焦点高亮(v0.4.55)。
|
|
595
|
+
// 独立 state:它纯粹是瞬时交互反馈,与配置无关,
|
|
596
|
+
// 也不该因为保存配置(25 处 set 重建主 state)被重置。
|
|
597
|
+
var hvst = React.useState({ id: "" })
|
|
598
|
+
var hvset = hvst[1]
|
|
599
|
+
var advHover = hvst[0].id || ""
|
|
600
|
+
|
|
496
601
|
var st = React.useState({
|
|
497
602
|
loaded: false,
|
|
498
603
|
values: {
|
|
@@ -935,6 +1040,17 @@ window.__ModuleLoader__.load({
|
|
|
935
1040
|
h('span', { style: S.hint }, '关掉只弹 Toast'),
|
|
936
1041
|
))
|
|
937
1042
|
|
|
1043
|
+
// ⚠️ 两个都关掉 = 完全没有任何提醒(v0.4.55)。
|
|
1044
|
+
//
|
|
1045
|
+
// 用户指出这个歧义:不禁止这样设置(那是用户的选择),
|
|
1046
|
+
// 但必须**如实告知结果**,否则用户以为"我配过了怎么没反应"。
|
|
1047
|
+
// 用中性样式,不用红/黄 —— 这不是故障。
|
|
1048
|
+
if (v.showToast === false && v.voice === false) {
|
|
1049
|
+
rows.push(h('div', { key: 'all-silent', style: S.noticeBox },
|
|
1050
|
+
'弹窗与声音都已关闭 —— 插件不会产生任何可见或可听的提醒。'
|
|
1051
|
+
+ '(提醒规则里的事件仍会照常触发,只是没有任何表现。)'))
|
|
1052
|
+
}
|
|
1053
|
+
|
|
938
1054
|
// ── 音源选择(v0.4.4)─────────────────────────────────────────────────
|
|
939
1055
|
//
|
|
940
1056
|
// 这是本插件最核心的可配置项:用户想听系统语音、想听语音包、想要系统音效、
|
|
@@ -1083,7 +1199,7 @@ window.__ModuleLoader__.load({
|
|
|
1083
1199
|
['done', '完成'], ['fail', '失败'], ['question', '提问'],
|
|
1084
1200
|
['approval', '批准'], ['blocked', '拦截'],
|
|
1085
1201
|
]
|
|
1086
|
-
|
|
1202
|
+
return h('div', { style: S.pvGroup },
|
|
1087
1203
|
KINDS.map(function (pair) {
|
|
1088
1204
|
var isBusy = pvState.kind === pair[0]
|
|
1089
1205
|
return h('button', {
|
|
@@ -1175,7 +1291,8 @@ window.__ModuleLoader__.load({
|
|
|
1175
1291
|
// 条款要**下载前**可见 —— 不能只靠 zip 里的 TERMS.md(那要下载后才看得到)。
|
|
1176
1292
|
return h('div', { key: 'vp-item-' + p.key, style: { display: 'flex', flexDirection: 'column', gap: '2px', padding: '3px 0' } },
|
|
1177
1293
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1178
|
-
h('span', { style: { flex: '1' }
|
|
1294
|
+
h('span', { style: { flex: '1' }, title: p.key },
|
|
1295
|
+
(prettyPackName(p.name) || p.key) + (mb ? '(' + mb + ')' : '')),
|
|
1179
1296
|
p.installed ? h('span', { style: { color: '#2a7' } }, '已安装') : null,
|
|
1180
1297
|
h('button', {
|
|
1181
1298
|
type: 'button',
|
|
@@ -1242,7 +1359,10 @@ window.__ModuleLoader__.load({
|
|
|
1242
1359
|
value: v.voiceName || 'zundamon-normal',
|
|
1243
1360
|
disabled: s.busy || !fixedMode,
|
|
1244
1361
|
onChange: function (e) { apply({ voiceName: e.target.value }, '音色已更新') },
|
|
1245
|
-
}, keyOpts.map(function (k) {
|
|
1362
|
+
}, keyOpts.map(function (k) {
|
|
1363
|
+
// value 仍是原 key(保存用它),只改显示文本;原 key 放 title
|
|
1364
|
+
return h('option', { key: k, value: k, title: k }, voiceLabel(k))
|
|
1365
|
+
})),
|
|
1246
1366
|
fixedMode ? null : h('span', { style: S.hint }, '随机模式下不生效'),
|
|
1247
1367
|
))
|
|
1248
1368
|
}
|
|
@@ -1647,13 +1767,33 @@ window.__ModuleLoader__.load({
|
|
|
1647
1767
|
ADV_SECTIONS.forEach(function (sec) {
|
|
1648
1768
|
var id = sec[0]
|
|
1649
1769
|
var isOpen = advSec[id] === true
|
|
1770
|
+
// 整行可点 + 箭头固定最右 + 悬停/焦点反馈 + 键盘可操作(v0.4.55)。
|
|
1771
|
+
//
|
|
1772
|
+
// 用户反馈:原来箭头跟在文字后面(`高级设置 ▸`),不明显能点。
|
|
1773
|
+
// 现在整行是点击区、箭头靠右、Enter/Space 也能展开 ——
|
|
1774
|
+
// 键盘用户与鼠标用户看到同一套反馈(foldHeadActive)。
|
|
1775
|
+
var headActive = advHover === id
|
|
1650
1776
|
rows.push(h('div', {
|
|
1651
1777
|
key: 'adv-sec-' + id,
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1778
|
+
role: 'button',
|
|
1779
|
+
tabIndex: 0,
|
|
1780
|
+
'aria-expanded': isOpen ? 'true' : 'false',
|
|
1781
|
+
style: Object.assign({}, S.foldHead, headActive ? S.foldHeadActive : null),
|
|
1655
1782
|
onClick: function () { toggleAdvSec(id) },
|
|
1656
|
-
|
|
1783
|
+
onMouseEnter: function () { hvset({ id: id }) },
|
|
1784
|
+
onMouseLeave: function () { hvset({ id: advHover === id ? '' : advHover }) },
|
|
1785
|
+
onFocus: function () { hvset({ id: id }) },
|
|
1786
|
+
onBlur: function () { hvset({ id: advHover === id ? '' : advHover }) },
|
|
1787
|
+
onKeyDown: function (e) {
|
|
1788
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1789
|
+
e.preventDefault()
|
|
1790
|
+
toggleAdvSec(id)
|
|
1791
|
+
}
|
|
1792
|
+
},
|
|
1793
|
+
},
|
|
1794
|
+
h('span', { style: { fontWeight: 600, fontSize: '13px' } }, sec[1]),
|
|
1795
|
+
h('span', { style: S.foldArrow }, isOpen ? '▾' : '▸'),
|
|
1796
|
+
))
|
|
1657
1797
|
rows.push(h('div', { key: 'adv-sec-desc-' + id, style: S.subHint }, sec[2]))
|
|
1658
1798
|
|
|
1659
1799
|
if (isOpen) {
|
|
@@ -1871,7 +2011,9 @@ window.__ModuleLoader__.load({
|
|
|
1871
2011
|
return function (e) { var o = {}; o[k] = e.target.value; apply(o, '已更新') }
|
|
1872
2012
|
})(nKey),
|
|
1873
2013
|
}, [h('option', { key: '', value: '' }, '跟随全局')].concat(
|
|
1874
|
-
kOpts2.map(function (x) {
|
|
2014
|
+
kOpts2.map(function (x) {
|
|
2015
|
+
return h('option', { key: x, value: x, title: x }, voiceLabel(x))
|
|
2016
|
+
}),
|
|
1875
2017
|
)),
|
|
1876
2018
|
mCur === 'random' ? h('span', { style: S.hint }, '随机模式下不生效') : null,
|
|
1877
2019
|
))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notify-say",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.55",
|
|
4
4
|
"description": "Desktop notifications for DSH Desktop that actually speak: Windows TTS out of the box (zero download), VOICEVOX voice packs optional, a selectable system-sound fallback, and an idle gate that waits for running subagents before saying the turn is done. Includes a settings-page GUI (sound source, per-event overrides, jump-back-to-session, cooldowns) and a self hot-reload for development.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 音色友好显示名的一致性测试(v0.4.55)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要这些测试
|
|
5
|
+
*
|
|
6
|
+
* 用户反馈:下拉里显示 `zundamon-normal` 是开发者内部标识,应显示「角色名 · 风格名」。
|
|
7
|
+
*
|
|
8
|
+
* 这个改动本身很小,但它引入了**第二份音色名字**:
|
|
9
|
+
* ① `tools/build-voice-packs.ps1` 的 `$Approved[].Name`(打包用,权威)
|
|
10
|
+
* ② `lib/client.js` 的 `VOICE_LABELS`(显示用)
|
|
11
|
+
*
|
|
12
|
+
* 两份名字一旦漂移,用户会在「语音包列表」看到一个名字、
|
|
13
|
+
* 在「音色下拉」看到另一个名字 —— 同一件事两种叫法。
|
|
14
|
+
* 所以必须用测试把它们钉在一起。
|
|
15
|
+
*
|
|
16
|
+
* ## 关键的"不能改"约束
|
|
17
|
+
*
|
|
18
|
+
* 只改显示。**保存提交的仍是原 key** —— 如果哪天有人把 option 的 value
|
|
19
|
+
* 改成友好名,用户的配置就会写入一个后端不认识的字符串。
|
|
20
|
+
* Q3 专门盯这一条。
|
|
21
|
+
*/
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
25
|
+
import { resolve, dirname, join } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const CLIENT = readFileSync(join(ROOT, 'lib', 'client.js'), 'utf8');
|
|
30
|
+
const BUILD_PS1 = join(ROOT, 'tools', 'build-voice-packs.ps1');
|
|
31
|
+
const hasBuild = existsSync(BUILD_PS1);
|
|
32
|
+
|
|
33
|
+
/** 解析 client.js 里的 VOICE_KEYS。 */
|
|
34
|
+
function voiceKeys() {
|
|
35
|
+
const m = CLIENT.match(/var VOICE_KEYS = \[([\s\S]*?)\n \]/);
|
|
36
|
+
assert.ok(m, '应能定位 VOICE_KEYS');
|
|
37
|
+
return [...m[1].matchAll(/'([a-z0-9-]+)'/g)].map((x) => x[1]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 解析 client.js 里的 VOICE_LABELS。 */
|
|
41
|
+
function voiceLabels() {
|
|
42
|
+
const m = CLIENT.match(/var VOICE_LABELS = \{([\s\S]*?)\n \}/);
|
|
43
|
+
assert.ok(m, '应能定位 VOICE_LABELS');
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const mm of m[1].matchAll(/'([a-z0-9-]+)':\s*'([^']+)'/g)) out[mm[1]] = mm[2];
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 解析打包脚本的 $Approved(key → 官方 Name)。 */
|
|
50
|
+
function approvedNames() {
|
|
51
|
+
const src = readFileSync(BUILD_PS1, 'utf8');
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const m of src.matchAll(/'([a-z0-9-]+)'\s*=\s*@\{\s*Name\s*=\s*'([^']+)'/g)) {
|
|
54
|
+
out[m[1]] = m[2];
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `ずんだもん(ノーマル)` → `ずんだもん`;`四国めたん` → `四国めたん`。 */
|
|
60
|
+
function characterOf(official) {
|
|
61
|
+
const m = /^(.+?)((.+?))$/.exec(official);
|
|
62
|
+
return m ? m[1] : official;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Q1: VOICE_KEYS 每一项都有友好名 ─────────────────────────────────────
|
|
66
|
+
test('Q1: 每个可选音色都有友好显示名', () => {
|
|
67
|
+
const keys = voiceKeys();
|
|
68
|
+
const labels = voiceLabels();
|
|
69
|
+
assert.ok(keys.length >= 8, `音色数量偏少(${keys.length})`);
|
|
70
|
+
|
|
71
|
+
const missing = keys.filter((k) => !labels[k]);
|
|
72
|
+
assert.deepEqual(missing, [],
|
|
73
|
+
'这些音色还显示着英文 key(用户看不懂),应补进 VOICE_LABELS:\n' + missing.join('\n'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── Q2: 友好名与打包脚本的官方名同源 ────────────────────────────────────
|
|
77
|
+
test('Q2: 友好名的角色部分与 build-voice-packs.ps1 一致', () => {
|
|
78
|
+
if (!hasBuild) {
|
|
79
|
+
// 发布包里没有维护者工具,跳过(与 D6/D7 同款处理)
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const labels = voiceLabels();
|
|
83
|
+
const approved = approvedNames();
|
|
84
|
+
assert.ok(Object.keys(approved).length >= 8, '应能解析出打包脚本的音色表');
|
|
85
|
+
|
|
86
|
+
const problems = [];
|
|
87
|
+
for (const [key, official] of Object.entries(approved)) {
|
|
88
|
+
const label = labels[key];
|
|
89
|
+
if (!label) continue; // Q1 管缺失
|
|
90
|
+
const wantChar = characterOf(official);
|
|
91
|
+
const gotChar = label.split(' · ')[0];
|
|
92
|
+
if (gotChar !== wantChar) {
|
|
93
|
+
problems.push(`${key}: 显示「${label}」,但打包脚本的角色名是「${wantChar}」`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
assert.deepEqual(problems, [],
|
|
97
|
+
'两份音色名字漂移了 —— 用户会在不同位置看到不同叫法:\n' + problems.join('\n'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── Q3: 保存的仍是原 key,不是友好名 ───────────────────────────────────
|
|
101
|
+
test('Q3: 音色下拉的 value 仍是原 key(只改显示)', () => {
|
|
102
|
+
// 如果 value 被改成友好名,用户的配置会写入后端不认识的字符串。
|
|
103
|
+
assert.match(CLIENT, /h\('option', \{ key: k, value: k, title: k \}, voiceLabel\(k\)\)/,
|
|
104
|
+
'固定音色的 option 必须 value=原key、显示=友好名');
|
|
105
|
+
assert.match(CLIENT, /h\('option', \{ key: x, value: x, title: x \}, voiceLabel\(x\)\)/,
|
|
106
|
+
'按事件覆盖的音色 option 同理');
|
|
107
|
+
// 原 key 要能在悬停提示里看到(用户排查问题时需要)
|
|
108
|
+
assert.match(CLIENT, /title: k/, '原 key 应放在 title 里(悬停可见)');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── Q4: option 显示文本不再直接是 key ──────────────────────────────────
|
|
112
|
+
test('Q4: 音色 option 的显示文本走 voiceLabel,不是裸 key', () => {
|
|
113
|
+
// 反面断言:不能再出现 `h('option', { key: k, value: k }, k)` 这种裸 key 显示
|
|
114
|
+
assert.doesNotMatch(CLIENT, /h\('option', \{ key: k, value: k \}, k\)/,
|
|
115
|
+
'固定音色不应再直接显示英文 key');
|
|
116
|
+
assert.doesNotMatch(CLIENT, /h\('option', \{ key: x, value: x \}, x\)/,
|
|
117
|
+
'按事件覆盖的音色不应再直接显示英文 key');
|
|
118
|
+
|
|
119
|
+
// 语音包列表也要用统一风格(括号 → 点)
|
|
120
|
+
assert.match(CLIENT, /prettyPackName\(p\.name\)/,
|
|
121
|
+
'语音包列表的官方名应统一成「角色 · 风格」风格');
|
|
122
|
+
assert.match(CLIENT, /function prettyPackName/, '应有 prettyPackName 工具函数');
|
|
123
|
+
});
|