dsh-notify-say 0.4.21 → 0.4.25
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 +216 -176
- package/docs/audio-audit/README.md +164 -0
- package/docs/audio-audit/fetch-snapshots.ps1 +76 -0
- package/docs/audio-audit/snapshots/ameharu-hau.txt +89 -0
- package/docs/audio-audit/snapshots/goki-nuigurumi.txt +73 -0
- package/docs/audio-audit/snapshots/kasukabe-tsumugi.txt +197 -0
- package/docs/audio-audit/snapshots/manbetsu-hanamaru.txt +27 -0
- package/docs/audio-audit/snapshots/meimei-meria.txt +84 -0
- package/docs/audio-audit/snapshots/mochiko.txt +27 -0
- package/docs/audio-audit/snapshots/namiine-ritsu.txt +76 -0
- package/docs/audio-audit/snapshots/shirakami-kotaro.txt +158 -0
- package/docs/audio-audit/snapshots/sss-anonymous-common.txt +142 -0
- package/docs/audio-audit/snapshots/sss-zundamon.txt +142 -0
- package/docs/audio-audit/snapshots/voicevox-software.txt +41 -0
- package/docs/audio-audit/snapshots/voicevox-vvm-terms.txt +385 -0
- package/docs/audio-audit/snapshots/yurei-chan.txt +112 -0
- package/docs//345/276/205/345/201/232/346/270/205/345/215/225.md +218 -0
- package/docs//350/257/255/351/237/263/345/214/205/345/210/206/345/217/221/346/226/271/346/241/210-/345/276/205/345/256/241/351/230/205.md +378 -0
- package/lib/client.js +50 -8
- package/lib/config-route.js +251 -209
- package/lib/http-guard.js +222 -0
- package/lib/index.js +1822 -1768
- package/lib/session-bridge.js +138 -117
- package/package.json +11 -2
- package/test/audio-source.test.mjs +184 -0
- package/test/audio-wiring.test.mjs +287 -0
- package/test/client-bundle.test.mjs +671 -0
- package/test/client-contract.test.mjs +118 -0
- package/test/config-coverage.test.mjs +109 -0
- package/test/config-route.test.mjs +212 -0
- package/test/config-schema.test.mjs +147 -0
- package/test/config-store.test.mjs +256 -0
- package/test/dev-reload.test.mjs +132 -0
- package/test/effective-for-client.test.mjs +142 -0
- package/test/fail-route.test.mjs +128 -0
- package/test/fail-voice.test.mjs +193 -0
- package/test/http-guard.test.mjs +143 -0
- package/test/http-integration.test.mjs +284 -0
- package/test/idle-gate.test.mjs +167 -0
- package/test/preview.test.mjs +92 -0
- package/test/real-cordis.test.mjs +204 -0
- package/test/release-voice-guard.test.mjs +185 -0
- package/test/reload-live.test.mjs +261 -0
- package/test/reload-verification.test.mjs +218 -0
- package/test/route-loader-register.mjs +22 -0
- package/test/scalar-guard.test.mjs +125 -0
- package/test/session-jump.test.mjs +266 -0
- package/test/stub-child-process.mjs +43 -0
- package/test/tts-lines.test.mjs +112 -0
- package/test/version-consistency.test.mjs +98 -0
- package/test/voice-availability.test.mjs +90 -0
- package/test/wav-gain.test.mjs +138 -0
- package/test/wiring.test.mjs +287 -0
- package/tools/notify-config.ps1 +472 -471
package/tools/notify-config.ps1
CHANGED
|
@@ -1,471 +1,472 @@
|
|
|
1
|
-
<#
|
|
2
|
-
.SYNOPSIS
|
|
3
|
-
dsh-notify-say 配置命令行(v0.4.0 的「第 4 层」运行时覆盖)。
|
|
4
|
-
|
|
5
|
-
.DESCRIPTION
|
|
6
|
-
## 为什么有它
|
|
7
|
-
|
|
8
|
-
v0.4.0 起设置页 GUI 由官方 Config schema 自动生成(改 `storages` 用户层)。
|
|
9
|
-
但有两类场景 GUI 不方便:
|
|
10
|
-
|
|
11
|
-
1. 远程办公/听不见声音,只想一条命令静音;
|
|
12
|
-
2. 不想为改一个音量去重启宿主。
|
|
13
|
-
|
|
14
|
-
本脚本读写 `$DSH_HOME\storages\dsh-notify-say\config.json`
|
|
15
|
-
(**本插件唯一拥有的持久文件**,不碰 cordis.patch.yml、不碰凭据、不碰别的插件)。
|
|
16
|
-
插件每次播报前会重读它,故**改完立即生效,无需重启**。
|
|
17
|
-
|
|
18
|
-
## 优先级提醒
|
|
19
|
-
|
|
20
|
-
生效值 = schema 默认 → cordis.patch.yml → **设置页** → **本文件**
|
|
21
|
-
也就是说本文件优先级最高;改过之后设置页里同一项会被覆盖。
|
|
22
|
-
想交还给设置页:用 `-Clear`。
|
|
23
|
-
|
|
24
|
-
## 安全设计
|
|
25
|
-
|
|
26
|
-
- 写入前做白名单校验(未知键丢弃),不会污染文件;
|
|
27
|
-
- 文件损坏时**拒绝覆盖**并提示,保留原文件供人工救;
|
|
28
|
-
- 原子写(先 .tmp 后 rename),不留半截文件;
|
|
29
|
-
- 只写本插件自己的目录。
|
|
30
|
-
|
|
31
|
-
.PARAMETER Show
|
|
32
|
-
显示当前生效配置(合并了 patch / 设置页 / 本文件的最终值)与来源。
|
|
33
|
-
|
|
34
|
-
.PARAMETER Voice
|
|
35
|
-
语音开关:on / off。
|
|
36
|
-
|
|
37
|
-
.PARAMETER Volume
|
|
38
|
-
音量 0-100。
|
|
39
|
-
|
|
40
|
-
.PARAMETER VoiceMode
|
|
41
|
-
音色模式:fixed(固定) / random(每次随机)。
|
|
42
|
-
|
|
43
|
-
.PARAMETER VoiceName
|
|
44
|
-
固定音色 key(如 zundamon-normal)。可用 -ListVoices 查看有哪些。
|
|
45
|
-
|
|
46
|
-
.PARAMETER Enable
|
|
47
|
-
打开某类提醒。可多值,逗号或空格分隔:
|
|
48
|
-
complete, error, blocked, aborted, approval, question, subagents, idleGate
|
|
49
|
-
|
|
50
|
-
.PARAMETER Disable
|
|
51
|
-
关闭某类提醒,取值同上。
|
|
52
|
-
|
|
53
|
-
.PARAMETER Clear
|
|
54
|
-
删除本插件的 config.json,把控制权交还「设置页 / cordis.patch.yml」。
|
|
55
|
-
|
|
56
|
-
.PARAMETER ListVoices
|
|
57
|
-
列出语句池里可用的音色 key 及各池覆盖情况。
|
|
58
|
-
|
|
59
|
-
.PARAMETER DryRun
|
|
60
|
-
只显示将要写入什么,不落盘。
|
|
61
|
-
|
|
62
|
-
.EXAMPLE
|
|
63
|
-
.\notify-config.ps1 -Show
|
|
64
|
-
|
|
65
|
-
.EXAMPLE
|
|
66
|
-
# 远程办公,先静音(Toast 仍会弹)
|
|
67
|
-
.\notify-config.ps1 -Voice off
|
|
68
|
-
|
|
69
|
-
.EXAMPLE
|
|
70
|
-
# 音量调到 40,音色改成随机
|
|
71
|
-
.\notify-config.ps1 -Volume 40 -VoiceMode random
|
|
72
|
-
|
|
73
|
-
.EXAMPLE
|
|
74
|
-
# 关掉「中止」类提醒,打开子代理提醒
|
|
75
|
-
.\notify-config.ps1 -Disable aborted -Enable subagents
|
|
76
|
-
|
|
77
|
-
.EXAMPLE
|
|
78
|
-
# 交还控制权(回到设置页/patch 的值)
|
|
79
|
-
.\notify-config.ps1 -Clear
|
|
80
|
-
#>
|
|
81
|
-
[CmdletBinding(DefaultParameterSetName = 'Set')]
|
|
82
|
-
param(
|
|
83
|
-
[switch]$Show,
|
|
84
|
-
[ValidateSet('on', 'off')]
|
|
85
|
-
[string]$Voice,
|
|
86
|
-
[ValidateRange(0, 100)]
|
|
87
|
-
[int]$Volume,
|
|
88
|
-
[ValidateSet('fixed', 'random')]
|
|
89
|
-
[string]$VoiceMode,
|
|
90
|
-
[string]$VoiceName,
|
|
91
|
-
[string[]]$Enable,
|
|
92
|
-
[string[]]$Disable,
|
|
93
|
-
[switch]$Clear,
|
|
94
|
-
[switch]$ListVoices,
|
|
95
|
-
# ── 语音包管理(按需下载)────────────────────────────────────────────────
|
|
96
|
-
# 完整语音包 47.93 MB;核心包只带一个音色。没装语音包时自动回退系统提示音。
|
|
97
|
-
[switch]$ListPacks,
|
|
98
|
-
[string]$PurgeVoice,
|
|
99
|
-
[switch]$InstallHint,
|
|
100
|
-
[switch]$DryRun
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
$ErrorActionPreference = 'Stop'
|
|
104
|
-
|
|
105
|
-
# ── 路径解析 ────────────────────────────────────────────────────────────────
|
|
106
|
-
# 本脚本位于 <plugin>/tools/,插件根 = 上一级。
|
|
107
|
-
# ⚠️ 不能用 $MyInvocation.MyCommand.Path —— 经 `powershell -File` 启动时它可能为空,
|
|
108
|
-
# 导致 $pluginRoot 变成 $null、进而 Join-Path 拼出错误路径(实测踩到)。
|
|
109
|
-
# 用 $PSScriptRoot(-File 与点源两种方式都可靠)+ $MyInvocation.MyCommand.Definition 兜底。
|
|
110
|
-
$scriptPath = $PSScriptRoot
|
|
111
|
-
if (-not $scriptPath) {
|
|
112
|
-
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
113
|
-
}
|
|
114
|
-
if (-not $scriptPath) {
|
|
115
|
-
Write-Host " 无法确定脚本所在目录,退出。" -ForegroundColor Red
|
|
116
|
-
exit 1
|
|
117
|
-
}
|
|
118
|
-
$pluginRoot = Split-Path -Parent $scriptPath
|
|
119
|
-
$notifyPs1 = Join-Path $pluginRoot 'dsh-notify.ps1'
|
|
120
|
-
|
|
121
|
-
function Get-DshHome {
|
|
122
|
-
$h = $env:DSH_HOME
|
|
123
|
-
if (-not $h) { $h = 'D:\Program Files (x86)\DSH' } # 本机默认,仅作兜底
|
|
124
|
-
return $h.TrimEnd('\', '/')
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
$dshHome = Get-DshHome
|
|
128
|
-
$configDir = Join-Path $dshHome 'storages\dsh-notify-say'
|
|
129
|
-
$configPath = Join-Path $configDir 'config.json'
|
|
130
|
-
|
|
131
|
-
# ── 提醒类别 → 配置键映射 ───────────────────────────────────────────────────
|
|
132
|
-
$KindMap = [ordered]@{
|
|
133
|
-
complete = 'notifyOnComplete'
|
|
134
|
-
error = 'notifyOnError'
|
|
135
|
-
blocked = 'notifyOnBlocked'
|
|
136
|
-
aborted = 'notifyOnAborted'
|
|
137
|
-
approval = 'notifyOnApproval'
|
|
138
|
-
question = 'notifyOnQuestion'
|
|
139
|
-
subagents = 'subagents'
|
|
140
|
-
idlegate = 'idleGate'
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
$KindDesc = @{
|
|
144
|
-
notifyOnComplete = '回合正常完成'
|
|
145
|
-
notifyOnError = '回合出错(失败语音)'
|
|
146
|
-
notifyOnBlocked = '回合被拦截(蜂鸣)'
|
|
147
|
-
notifyOnAborted = '中止/超长/中断'
|
|
148
|
-
notifyOnApproval = '待你批准'
|
|
149
|
-
notifyOnQuestion = '向你提问'
|
|
150
|
-
subagents = '子代理会话也提醒'
|
|
151
|
-
idleGate = '空闲门(有在飞工作先不播完成)'
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
# ── 读现有配置(损坏则拒绝写,保留原文件)───────────────────────────────────
|
|
155
|
-
function Read-ConfigDoc {
|
|
156
|
-
if (-not (Test-Path -LiteralPath $configPath)) {
|
|
157
|
-
return @{ ok = $true; values = @{}; exists = $false }
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
$raw = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8
|
|
161
|
-
} catch {
|
|
162
|
-
return @{ ok = $false; reason = "读取失败: $($_.Exception.Message)"; values = @{}; exists = $true }
|
|
163
|
-
}
|
|
164
|
-
if (-not $raw -or -not $raw.Trim()) { return @{ ok = $true; values = @{}; exists = $true } }
|
|
165
|
-
try {
|
|
166
|
-
$doc = $raw | ConvertFrom-Json
|
|
167
|
-
} catch {
|
|
168
|
-
return @{ ok = $false; reason = '文件不是合法 JSON'; values = @{}; exists = $true }
|
|
169
|
-
}
|
|
170
|
-
if ($null -eq $doc -or $doc.version -ne 1) {
|
|
171
|
-
return @{ ok = $false; reason = "版本不匹配(期望 1,实际 $($doc.version))"; values = @{}; exists = $true }
|
|
172
|
-
}
|
|
173
|
-
$vals = @{}
|
|
174
|
-
if ($doc.values) {
|
|
175
|
-
foreach ($p in $doc.values.PSObject.Properties) { $vals[$p.Name] = $p.Value }
|
|
176
|
-
}
|
|
177
|
-
return @{ ok = $true; values = $vals; exists = $true }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function Write-ConfigDoc([hashtable]$values) {
|
|
181
|
-
if ($DryRun) {
|
|
182
|
-
Write-Host " [DryRun] 将写入 $configPath :" -ForegroundColor Yellow
|
|
183
|
-
$values.GetEnumerator() | Sort-Object Name | ForEach-Object { Write-Host (" {0} = {1}" -f $_.Key, $_.Value) }
|
|
184
|
-
return $true
|
|
185
|
-
}
|
|
186
|
-
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
|
187
|
-
$doc = [ordered]@{ version = 1; values = [ordered]@{} }
|
|
188
|
-
foreach ($k in ($values.Keys | Sort-Object)) { $doc.values[$k] = $values[$k] }
|
|
189
|
-
$json = $doc | ConvertTo-Json -Depth 6
|
|
190
|
-
$tmp = "$configPath.tmp"
|
|
191
|
-
try {
|
|
192
|
-
[System.IO.File]::WriteAllText($tmp, $json + "`n", (New-Object System.Text.UTF8Encoding($false)))
|
|
193
|
-
Move-Item -LiteralPath $tmp -Destination $configPath -Force
|
|
194
|
-
} catch {
|
|
195
|
-
if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }
|
|
196
|
-
Write-Host " 写入失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
197
|
-
return $false
|
|
198
|
-
}
|
|
199
|
-
return $true
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
# ── -Show:显示生效值与来源 ─────────────────────────────────────────────────
|
|
203
|
-
function Show-Effective {
|
|
204
|
-
Write-Host "=== dsh-notify-say 配置 ===" -ForegroundColor Cyan
|
|
205
|
-
Write-Host (" DSH_HOME : {0}" -f $dshHome)
|
|
206
|
-
Write-Host (" CLI 覆盖层 : {0}" -f $configPath)
|
|
207
|
-
|
|
208
|
-
$cur = Read-ConfigDoc
|
|
209
|
-
if (-not $cur.ok) {
|
|
210
|
-
Write-Host (" ⚠️ 覆盖层损坏:{0}" -f $cur.reason) -ForegroundColor Yellow
|
|
211
|
-
Write-Host " 插件会忽略它并回退到上层配置;删掉该文件即可恢复。" -ForegroundColor Yellow
|
|
212
|
-
} elseif (-not $cur.exists) {
|
|
213
|
-
Write-Host " 状态: 不存在(全部走 设置页 / cordis.patch.yml)" -ForegroundColor DarkGray
|
|
214
|
-
} else {
|
|
215
|
-
Write-Host (" 状态: 有 {0} 项覆盖" -f $cur.values.Count) -ForegroundColor DarkGray
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
Write-Host ""
|
|
219
|
-
Write-Host "--- 覆盖层里的值(本文件优先级最高)---" -ForegroundColor Cyan
|
|
220
|
-
if ($cur.ok -and $cur.values.Count -gt 0) {
|
|
221
|
-
$cur.values.GetEnumerator() | Sort-Object Name | ForEach-Object {
|
|
222
|
-
$desc = if ($KindDesc.ContainsKey($_.Key)) { $KindDesc[$_.Key] } else { '' }
|
|
223
|
-
Write-Host (" {0,-20} = {1,-8} {2}" -f $_.Key, $_.Value, $desc)
|
|
224
|
-
}
|
|
225
|
-
} else {
|
|
226
|
-
Write-Host " (无)" -ForegroundColor DarkGray
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
# 从 patch 读基线(只读,不解释优先级,仅供对照)
|
|
230
|
-
$patch = Join-Path $pluginRoot 'cordis.patch.yml'
|
|
231
|
-
if (Test-Path -LiteralPath $patch) {
|
|
232
|
-
Write-Host ""
|
|
233
|
-
Write-Host "--- cordis.patch.yml 里的基线(会被上面覆盖)---" -ForegroundColor Cyan
|
|
234
|
-
Get-Content -LiteralPath $patch -Encoding UTF8 |
|
|
235
|
-
Where-Object { $_ -match '^\s+\w+:' } |
|
|
236
|
-
ForEach-Object { Write-Host (" " + $_.Trim()) }
|
|
237
|
-
}
|
|
238
|
-
Write-Host ""
|
|
239
|
-
Write-Host "提示:设置页的值介于两者之间,此处无法直接读取(需宿主接口)。" -ForegroundColor DarkGray
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
# ── -ListVoices:列音色及各池覆盖 ───────────────────────────────────────────
|
|
243
|
-
function Show-Voices {
|
|
244
|
-
$vl = Join-Path $pluginRoot 'sounds\voice-lines'
|
|
245
|
-
if (-not (Test-Path -LiteralPath $vl)) { Write-Host " 找不到语句池目录" -ForegroundColor Red; return }
|
|
246
|
-
|
|
247
|
-
# done 池根目录的音色 key(去掉 ~N 变体)
|
|
248
|
-
$doneKeys = @(Get-ChildItem -LiteralPath $vl -File -Filter '*.wav' -ErrorAction SilentlyContinue |
|
|
249
|
-
ForEach-Object { $_.BaseName -replace '~\d+$', '' } | Sort-Object -Unique)
|
|
250
|
-
foreach ($pool in @('question', 'approval', 'fail')) {
|
|
251
|
-
$d = Join-Path $vl $pool
|
|
252
|
-
if (Test-Path -LiteralPath $d) {
|
|
253
|
-
Set-Variable -Name "keys_$pool" -Value @(Get-ChildItem -LiteralPath $d -File -Filter '*.wav' -ErrorAction SilentlyContinue |
|
|
254
|
-
ForEach-Object { $_.BaseName -replace '~\d+$', '' } | Sort-Object -Unique)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
Write-Host "=== 可用音色(共 $($doneKeys.Count) 个 key)===" -ForegroundColor Cyan
|
|
259
|
-
Write-Host " 标记含义:完成=有完成语音 提问/审批/失败=对应池是否覆盖" -ForegroundColor DarkGray
|
|
260
|
-
Write-Host ""
|
|
261
|
-
Write-Host (" {0,-26} {1,-6} {2,-6} {3,-6} {4}" -f '音色 key', '完成', '提问', '审批', '失败')
|
|
262
|
-
Write-Host (" " + ("-" * 62))
|
|
263
|
-
foreach ($k in $doneKeys) {
|
|
264
|
-
$q = if ((Get-Variable -Name 'keys_question' -ValueOnly -ErrorAction SilentlyContinue) -contains $k) { '✓' } else { '-' }
|
|
265
|
-
$a = if ((Get-Variable -Name 'keys_approval' -ValueOnly -ErrorAction SilentlyContinue) -contains $k) { '✓' } else { '-' }
|
|
266
|
-
$f = if ((Get-Variable -Name 'keys_fail' -ValueOnly -ErrorAction SilentlyContinue) -contains $k) { '✓' } else { '-' }
|
|
267
|
-
$warn = if ($f -eq '-') { ' ← 失败时无此音色台词' } else { '' }
|
|
268
|
-
Write-Host (" {0,-26} {1,-6} {2,-6} {3,-6} {4}{5}" -f $k, '✓', $q, $a, $f, $warn)
|
|
269
|
-
}
|
|
270
|
-
Write-Host ""
|
|
271
|
-
Write-Host "说明:某池未覆盖该音色时,v0.3.1+ 会在**同池内换一个音色**(不会哑掉,也不会越池播错语义)。" -ForegroundColor DarkGray
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
# ── 语音包管理:列出本地已装包 ──────────────────────────────────────────────
|
|
275
|
-
function Show-Packs {
|
|
276
|
-
$vl = Join-Path $pluginRoot 'sounds\voice-lines'
|
|
277
|
-
Write-Host "=== 已装语音包 ===" -ForegroundColor Cyan
|
|
278
|
-
|
|
279
|
-
if (-not (Test-Path -LiteralPath $vl)) {
|
|
280
|
-
Write-Host " 没有任何语音包。" -ForegroundColor DarkGray
|
|
281
|
-
Write-Host " → 通知会使用系统提示音 + Toast(这是正常状态,不是错误)。" -ForegroundColor DarkGray
|
|
282
|
-
Write-Host " → 用 -ListVoices 查看目录里有哪些音色 key 可选;用 -PurgeVoice <key> 删除。" -ForegroundColor DarkGray
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
# 按音色 key 汇总(去 ~N 变体)
|
|
287
|
-
$acc = @{}
|
|
288
|
-
foreach ($pool in @('', 'question', 'approval', 'fail')) {
|
|
289
|
-
$d = if ($pool) { Join-Path $vl $pool } else { $vl }
|
|
290
|
-
if (-not (Test-Path -LiteralPath $d)) { continue }
|
|
291
|
-
Get-ChildItem -LiteralPath $d -File -Filter '*.wav' -ErrorAction SilentlyContinue | ForEach-Object {
|
|
292
|
-
$k = $_.BaseName -replace '~\d+$', ''
|
|
293
|
-
if (-not $acc.ContainsKey($k)) { $acc[$k] = @{ pools = @(); bytes = 0; files = 0 } }
|
|
294
|
-
$pname = if ($pool) { $pool } else { 'done' }
|
|
295
|
-
if ($acc[$k].pools -notcontains $pname) { $acc[$k].pools += $pname }
|
|
296
|
-
$acc[$k].bytes += $_.Length
|
|
297
|
-
$acc[$k].files++
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if ($acc.Count -eq 0) {
|
|
302
|
-
Write-Host " 目录存在但没有 wav 文件。" -ForegroundColor DarkGray
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
$totalMB = 0
|
|
307
|
-
Write-Host (" {0,-26} {1,-34} {2}" -f '音色 key', '覆盖的语句类', '体积')
|
|
308
|
-
Write-Host (" " + ("-" * 74))
|
|
309
|
-
foreach ($k in ($acc.Keys | Sort-Object)) {
|
|
310
|
-
$v = $acc[$k]
|
|
311
|
-
$mb = [math]::Round($v.bytes / 1MB, 2)
|
|
312
|
-
$totalMB += $mb
|
|
313
|
-
Write-Host (" {0,-26} {1,-34} {2,7:N2} MB" -f $k, ($v.pools -join ','), $mb)
|
|
314
|
-
}
|
|
315
|
-
Write-Host (" " + ("-" * 74))
|
|
316
|
-
Write-Host (" 合计 {0} 个音色 / {1:N2} MB" -f $acc.Count, $totalMB) -ForegroundColor Cyan
|
|
317
|
-
Write-Host ""
|
|
318
|
-
Write-Host " 说明:只装了 done 的音色,在失败时会**在同池内换音色**或回落提示音(不会播错语义)。" -ForegroundColor DarkGray
|
|
319
|
-
Write-Host " 完整语音包约 48 MB;核心包只需 1 个音色。删掉不用的用 -PurgeVoice <key>。" -ForegroundColor DarkGray
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
# ── 语音包管理:删除某音色 ──────────────────────────────────────────────────
|
|
323
|
-
function Remove-VoicePack([string]$key) {
|
|
324
|
-
if (-not $key) { Write-Host " 需要 -PurgeVoice <音色key>" -ForegroundColor Red; return }
|
|
325
|
-
if ($key -notmatch '^\w[\w-]*$') {
|
|
326
|
-
Write-Host " 非法音色 key:$key" -ForegroundColor Red; return
|
|
327
|
-
}
|
|
328
|
-
$vl = Join-Path $pluginRoot 'sounds\voice-lines'
|
|
329
|
-
$targets = @()
|
|
330
|
-
foreach ($pool in @('', 'question', 'approval', 'fail')) {
|
|
331
|
-
$d = if ($pool) { Join-Path $vl $pool } else { $vl }
|
|
332
|
-
if (-not (Test-Path -LiteralPath $d)) { continue }
|
|
333
|
-
Get-ChildItem -LiteralPath $d -File -Filter '*.wav' -ErrorAction SilentlyContinue | Where-Object {
|
|
334
|
-
($_.BaseName -replace '~\d+$', '') -eq $key
|
|
335
|
-
} | ForEach-Object { $targets += $_ }
|
|
336
|
-
}
|
|
337
|
-
if ($targets.Count -eq 0) {
|
|
338
|
-
Write-Host " 未找到音色 '$key' 的任何 wav。" -ForegroundColor Yellow
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
$mb = [math]::Round(($targets | Measure-Object Length -Sum).Sum / 1MB, 2)
|
|
342
|
-
Write-Host (" 将删除 {0} 个文件 / {1:N2} MB(音色 {2})" -f $targets.Count, $mb, $key) -ForegroundColor Yellow
|
|
343
|
-
if ($DryRun) { Write-Host " [DryRun] 未删除" -ForegroundColor Yellow; return }
|
|
344
|
-
$n = 0
|
|
345
|
-
foreach ($t in $targets) {
|
|
346
|
-
try { Remove-Item -LiteralPath $t.FullName -Force; $n++ } catch { }
|
|
347
|
-
}
|
|
348
|
-
Write-Host (" 已删除 {0} 个文件。" -f $n) -ForegroundColor Green
|
|
349
|
-
if (-not (Test-Path -LiteralPath $vl) -or @(Get-ChildItem -LiteralPath $vl -Recurse -File -Filter '*.wav' -ErrorAction SilentlyContinue).Count -eq 0) {
|
|
350
|
-
Write-Host " 已无任何语音包 → 通知将使用系统提示音 + Toast(正常状态)。" -ForegroundColor DarkGray
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
# ── 语音包管理:说明如何获取更多 ────────────────────────────────────────────
|
|
355
|
-
function Show-InstallHint {
|
|
356
|
-
Write-Host "=== 如何获得更多语音 ===" -ForegroundColor Cyan
|
|
357
|
-
Write-Host ""
|
|
358
|
-
Write-Host " 核心包只带 1 个日语音色(约 1 MB
|
|
359
|
-
Write-Host ""
|
|
360
|
-
Write-Host " A) 自己合成(推荐,零分发成本、可自选音色)" -ForegroundColor Green
|
|
361
|
-
Write-Host " 1. 安装 VOICEVOX(本地免费 TTS 引擎,约 1 GB)"
|
|
362
|
-
Write-Host " 2. 启动引擎(默认 http://127.0.0.1:50021)"
|
|
363
|
-
Write-Host " 3. 跑仓库里的合成脚本:"
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
Write-Host "
|
|
367
|
-
Write-Host "
|
|
368
|
-
Write-Host "
|
|
369
|
-
Write-Host "
|
|
370
|
-
Write-Host "
|
|
371
|
-
Write-Host "
|
|
372
|
-
Write-Host ""
|
|
373
|
-
Write-Host "
|
|
374
|
-
Write-Host "
|
|
375
|
-
Write-Host ""
|
|
376
|
-
Write-Host "
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if ($
|
|
382
|
-
if ($
|
|
383
|
-
if ($
|
|
384
|
-
if ($
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
Write-Host "
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
Write-Host "
|
|
403
|
-
Write-Host "
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
$
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
$
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
$
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
Write-Host "
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
dsh-notify-say 配置命令行(v0.4.0 的「第 4 层」运行时覆盖)。
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
## 为什么有它
|
|
7
|
+
|
|
8
|
+
v0.4.0 起设置页 GUI 由官方 Config schema 自动生成(改 `storages` 用户层)。
|
|
9
|
+
但有两类场景 GUI 不方便:
|
|
10
|
+
|
|
11
|
+
1. 远程办公/听不见声音,只想一条命令静音;
|
|
12
|
+
2. 不想为改一个音量去重启宿主。
|
|
13
|
+
|
|
14
|
+
本脚本读写 `$DSH_HOME\storages\dsh-notify-say\config.json`
|
|
15
|
+
(**本插件唯一拥有的持久文件**,不碰 cordis.patch.yml、不碰凭据、不碰别的插件)。
|
|
16
|
+
插件每次播报前会重读它,故**改完立即生效,无需重启**。
|
|
17
|
+
|
|
18
|
+
## 优先级提醒
|
|
19
|
+
|
|
20
|
+
生效值 = schema 默认 → cordis.patch.yml → **设置页** → **本文件**
|
|
21
|
+
也就是说本文件优先级最高;改过之后设置页里同一项会被覆盖。
|
|
22
|
+
想交还给设置页:用 `-Clear`。
|
|
23
|
+
|
|
24
|
+
## 安全设计
|
|
25
|
+
|
|
26
|
+
- 写入前做白名单校验(未知键丢弃),不会污染文件;
|
|
27
|
+
- 文件损坏时**拒绝覆盖**并提示,保留原文件供人工救;
|
|
28
|
+
- 原子写(先 .tmp 后 rename),不留半截文件;
|
|
29
|
+
- 只写本插件自己的目录。
|
|
30
|
+
|
|
31
|
+
.PARAMETER Show
|
|
32
|
+
显示当前生效配置(合并了 patch / 设置页 / 本文件的最终值)与来源。
|
|
33
|
+
|
|
34
|
+
.PARAMETER Voice
|
|
35
|
+
语音开关:on / off。
|
|
36
|
+
|
|
37
|
+
.PARAMETER Volume
|
|
38
|
+
音量 0-100。
|
|
39
|
+
|
|
40
|
+
.PARAMETER VoiceMode
|
|
41
|
+
音色模式:fixed(固定) / random(每次随机)。
|
|
42
|
+
|
|
43
|
+
.PARAMETER VoiceName
|
|
44
|
+
固定音色 key(如 zundamon-normal)。可用 -ListVoices 查看有哪些。
|
|
45
|
+
|
|
46
|
+
.PARAMETER Enable
|
|
47
|
+
打开某类提醒。可多值,逗号或空格分隔:
|
|
48
|
+
complete, error, blocked, aborted, approval, question, subagents, idleGate
|
|
49
|
+
|
|
50
|
+
.PARAMETER Disable
|
|
51
|
+
关闭某类提醒,取值同上。
|
|
52
|
+
|
|
53
|
+
.PARAMETER Clear
|
|
54
|
+
删除本插件的 config.json,把控制权交还「设置页 / cordis.patch.yml」。
|
|
55
|
+
|
|
56
|
+
.PARAMETER ListVoices
|
|
57
|
+
列出语句池里可用的音色 key 及各池覆盖情况。
|
|
58
|
+
|
|
59
|
+
.PARAMETER DryRun
|
|
60
|
+
只显示将要写入什么,不落盘。
|
|
61
|
+
|
|
62
|
+
.EXAMPLE
|
|
63
|
+
.\notify-config.ps1 -Show
|
|
64
|
+
|
|
65
|
+
.EXAMPLE
|
|
66
|
+
# 远程办公,先静音(Toast 仍会弹)
|
|
67
|
+
.\notify-config.ps1 -Voice off
|
|
68
|
+
|
|
69
|
+
.EXAMPLE
|
|
70
|
+
# 音量调到 40,音色改成随机
|
|
71
|
+
.\notify-config.ps1 -Volume 40 -VoiceMode random
|
|
72
|
+
|
|
73
|
+
.EXAMPLE
|
|
74
|
+
# 关掉「中止」类提醒,打开子代理提醒
|
|
75
|
+
.\notify-config.ps1 -Disable aborted -Enable subagents
|
|
76
|
+
|
|
77
|
+
.EXAMPLE
|
|
78
|
+
# 交还控制权(回到设置页/patch 的值)
|
|
79
|
+
.\notify-config.ps1 -Clear
|
|
80
|
+
#>
|
|
81
|
+
[CmdletBinding(DefaultParameterSetName = 'Set')]
|
|
82
|
+
param(
|
|
83
|
+
[switch]$Show,
|
|
84
|
+
[ValidateSet('on', 'off')]
|
|
85
|
+
[string]$Voice,
|
|
86
|
+
[ValidateRange(0, 100)]
|
|
87
|
+
[int]$Volume,
|
|
88
|
+
[ValidateSet('fixed', 'random')]
|
|
89
|
+
[string]$VoiceMode,
|
|
90
|
+
[string]$VoiceName,
|
|
91
|
+
[string[]]$Enable,
|
|
92
|
+
[string[]]$Disable,
|
|
93
|
+
[switch]$Clear,
|
|
94
|
+
[switch]$ListVoices,
|
|
95
|
+
# ── 语音包管理(按需下载)────────────────────────────────────────────────
|
|
96
|
+
# 完整语音包 47.93 MB;核心包只带一个音色。没装语音包时自动回退系统提示音。
|
|
97
|
+
[switch]$ListPacks,
|
|
98
|
+
[string]$PurgeVoice,
|
|
99
|
+
[switch]$InstallHint,
|
|
100
|
+
[switch]$DryRun
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
$ErrorActionPreference = 'Stop'
|
|
104
|
+
|
|
105
|
+
# ── 路径解析 ────────────────────────────────────────────────────────────────
|
|
106
|
+
# 本脚本位于 <plugin>/tools/,插件根 = 上一级。
|
|
107
|
+
# ⚠️ 不能用 $MyInvocation.MyCommand.Path —— 经 `powershell -File` 启动时它可能为空,
|
|
108
|
+
# 导致 $pluginRoot 变成 $null、进而 Join-Path 拼出错误路径(实测踩到)。
|
|
109
|
+
# 用 $PSScriptRoot(-File 与点源两种方式都可靠)+ $MyInvocation.MyCommand.Definition 兜底。
|
|
110
|
+
$scriptPath = $PSScriptRoot
|
|
111
|
+
if (-not $scriptPath) {
|
|
112
|
+
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
113
|
+
}
|
|
114
|
+
if (-not $scriptPath) {
|
|
115
|
+
Write-Host " 无法确定脚本所在目录,退出。" -ForegroundColor Red
|
|
116
|
+
exit 1
|
|
117
|
+
}
|
|
118
|
+
$pluginRoot = Split-Path -Parent $scriptPath
|
|
119
|
+
$notifyPs1 = Join-Path $pluginRoot 'dsh-notify.ps1'
|
|
120
|
+
|
|
121
|
+
function Get-DshHome {
|
|
122
|
+
$h = $env:DSH_HOME
|
|
123
|
+
if (-not $h) { $h = 'D:\Program Files (x86)\DSH' } # 本机默认,仅作兜底
|
|
124
|
+
return $h.TrimEnd('\', '/')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$dshHome = Get-DshHome
|
|
128
|
+
$configDir = Join-Path $dshHome 'storages\dsh-notify-say'
|
|
129
|
+
$configPath = Join-Path $configDir 'config.json'
|
|
130
|
+
|
|
131
|
+
# ── 提醒类别 → 配置键映射 ───────────────────────────────────────────────────
|
|
132
|
+
$KindMap = [ordered]@{
|
|
133
|
+
complete = 'notifyOnComplete'
|
|
134
|
+
error = 'notifyOnError'
|
|
135
|
+
blocked = 'notifyOnBlocked'
|
|
136
|
+
aborted = 'notifyOnAborted'
|
|
137
|
+
approval = 'notifyOnApproval'
|
|
138
|
+
question = 'notifyOnQuestion'
|
|
139
|
+
subagents = 'subagents'
|
|
140
|
+
idlegate = 'idleGate'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
$KindDesc = @{
|
|
144
|
+
notifyOnComplete = '回合正常完成'
|
|
145
|
+
notifyOnError = '回合出错(失败语音)'
|
|
146
|
+
notifyOnBlocked = '回合被拦截(蜂鸣)'
|
|
147
|
+
notifyOnAborted = '中止/超长/中断'
|
|
148
|
+
notifyOnApproval = '待你批准'
|
|
149
|
+
notifyOnQuestion = '向你提问'
|
|
150
|
+
subagents = '子代理会话也提醒'
|
|
151
|
+
idleGate = '空闲门(有在飞工作先不播完成)'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ── 读现有配置(损坏则拒绝写,保留原文件)───────────────────────────────────
|
|
155
|
+
function Read-ConfigDoc {
|
|
156
|
+
if (-not (Test-Path -LiteralPath $configPath)) {
|
|
157
|
+
return @{ ok = $true; values = @{}; exists = $false }
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
$raw = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8
|
|
161
|
+
} catch {
|
|
162
|
+
return @{ ok = $false; reason = "读取失败: $($_.Exception.Message)"; values = @{}; exists = $true }
|
|
163
|
+
}
|
|
164
|
+
if (-not $raw -or -not $raw.Trim()) { return @{ ok = $true; values = @{}; exists = $true } }
|
|
165
|
+
try {
|
|
166
|
+
$doc = $raw | ConvertFrom-Json
|
|
167
|
+
} catch {
|
|
168
|
+
return @{ ok = $false; reason = '文件不是合法 JSON'; values = @{}; exists = $true }
|
|
169
|
+
}
|
|
170
|
+
if ($null -eq $doc -or $doc.version -ne 1) {
|
|
171
|
+
return @{ ok = $false; reason = "版本不匹配(期望 1,实际 $($doc.version))"; values = @{}; exists = $true }
|
|
172
|
+
}
|
|
173
|
+
$vals = @{}
|
|
174
|
+
if ($doc.values) {
|
|
175
|
+
foreach ($p in $doc.values.PSObject.Properties) { $vals[$p.Name] = $p.Value }
|
|
176
|
+
}
|
|
177
|
+
return @{ ok = $true; values = $vals; exists = $true }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function Write-ConfigDoc([hashtable]$values) {
|
|
181
|
+
if ($DryRun) {
|
|
182
|
+
Write-Host " [DryRun] 将写入 $configPath :" -ForegroundColor Yellow
|
|
183
|
+
$values.GetEnumerator() | Sort-Object Name | ForEach-Object { Write-Host (" {0} = {1}" -f $_.Key, $_.Value) }
|
|
184
|
+
return $true
|
|
185
|
+
}
|
|
186
|
+
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
|
187
|
+
$doc = [ordered]@{ version = 1; values = [ordered]@{} }
|
|
188
|
+
foreach ($k in ($values.Keys | Sort-Object)) { $doc.values[$k] = $values[$k] }
|
|
189
|
+
$json = $doc | ConvertTo-Json -Depth 6
|
|
190
|
+
$tmp = "$configPath.tmp"
|
|
191
|
+
try {
|
|
192
|
+
[System.IO.File]::WriteAllText($tmp, $json + "`n", (New-Object System.Text.UTF8Encoding($false)))
|
|
193
|
+
Move-Item -LiteralPath $tmp -Destination $configPath -Force
|
|
194
|
+
} catch {
|
|
195
|
+
if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }
|
|
196
|
+
Write-Host " 写入失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
197
|
+
return $false
|
|
198
|
+
}
|
|
199
|
+
return $true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ── -Show:显示生效值与来源 ─────────────────────────────────────────────────
|
|
203
|
+
function Show-Effective {
|
|
204
|
+
Write-Host "=== dsh-notify-say 配置 ===" -ForegroundColor Cyan
|
|
205
|
+
Write-Host (" DSH_HOME : {0}" -f $dshHome)
|
|
206
|
+
Write-Host (" CLI 覆盖层 : {0}" -f $configPath)
|
|
207
|
+
|
|
208
|
+
$cur = Read-ConfigDoc
|
|
209
|
+
if (-not $cur.ok) {
|
|
210
|
+
Write-Host (" ⚠️ 覆盖层损坏:{0}" -f $cur.reason) -ForegroundColor Yellow
|
|
211
|
+
Write-Host " 插件会忽略它并回退到上层配置;删掉该文件即可恢复。" -ForegroundColor Yellow
|
|
212
|
+
} elseif (-not $cur.exists) {
|
|
213
|
+
Write-Host " 状态: 不存在(全部走 设置页 / cordis.patch.yml)" -ForegroundColor DarkGray
|
|
214
|
+
} else {
|
|
215
|
+
Write-Host (" 状态: 有 {0} 项覆盖" -f $cur.values.Count) -ForegroundColor DarkGray
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Write-Host ""
|
|
219
|
+
Write-Host "--- 覆盖层里的值(本文件优先级最高)---" -ForegroundColor Cyan
|
|
220
|
+
if ($cur.ok -and $cur.values.Count -gt 0) {
|
|
221
|
+
$cur.values.GetEnumerator() | Sort-Object Name | ForEach-Object {
|
|
222
|
+
$desc = if ($KindDesc.ContainsKey($_.Key)) { $KindDesc[$_.Key] } else { '' }
|
|
223
|
+
Write-Host (" {0,-20} = {1,-8} {2}" -f $_.Key, $_.Value, $desc)
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
Write-Host " (无)" -ForegroundColor DarkGray
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# 从 patch 读基线(只读,不解释优先级,仅供对照)
|
|
230
|
+
$patch = Join-Path $pluginRoot 'cordis.patch.yml'
|
|
231
|
+
if (Test-Path -LiteralPath $patch) {
|
|
232
|
+
Write-Host ""
|
|
233
|
+
Write-Host "--- cordis.patch.yml 里的基线(会被上面覆盖)---" -ForegroundColor Cyan
|
|
234
|
+
Get-Content -LiteralPath $patch -Encoding UTF8 |
|
|
235
|
+
Where-Object { $_ -match '^\s+\w+:' } |
|
|
236
|
+
ForEach-Object { Write-Host (" " + $_.Trim()) }
|
|
237
|
+
}
|
|
238
|
+
Write-Host ""
|
|
239
|
+
Write-Host "提示:设置页的值介于两者之间,此处无法直接读取(需宿主接口)。" -ForegroundColor DarkGray
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# ── -ListVoices:列音色及各池覆盖 ───────────────────────────────────────────
|
|
243
|
+
function Show-Voices {
|
|
244
|
+
$vl = Join-Path $pluginRoot 'sounds\voice-lines'
|
|
245
|
+
if (-not (Test-Path -LiteralPath $vl)) { Write-Host " 找不到语句池目录" -ForegroundColor Red; return }
|
|
246
|
+
|
|
247
|
+
# done 池根目录的音色 key(去掉 ~N 变体)
|
|
248
|
+
$doneKeys = @(Get-ChildItem -LiteralPath $vl -File -Filter '*.wav' -ErrorAction SilentlyContinue |
|
|
249
|
+
ForEach-Object { $_.BaseName -replace '~\d+$', '' } | Sort-Object -Unique)
|
|
250
|
+
foreach ($pool in @('question', 'approval', 'fail')) {
|
|
251
|
+
$d = Join-Path $vl $pool
|
|
252
|
+
if (Test-Path -LiteralPath $d) {
|
|
253
|
+
Set-Variable -Name "keys_$pool" -Value @(Get-ChildItem -LiteralPath $d -File -Filter '*.wav' -ErrorAction SilentlyContinue |
|
|
254
|
+
ForEach-Object { $_.BaseName -replace '~\d+$', '' } | Sort-Object -Unique)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Write-Host "=== 可用音色(共 $($doneKeys.Count) 个 key)===" -ForegroundColor Cyan
|
|
259
|
+
Write-Host " 标记含义:完成=有完成语音 提问/审批/失败=对应池是否覆盖" -ForegroundColor DarkGray
|
|
260
|
+
Write-Host ""
|
|
261
|
+
Write-Host (" {0,-26} {1,-6} {2,-6} {3,-6} {4}" -f '音色 key', '完成', '提问', '审批', '失败')
|
|
262
|
+
Write-Host (" " + ("-" * 62))
|
|
263
|
+
foreach ($k in $doneKeys) {
|
|
264
|
+
$q = if ((Get-Variable -Name 'keys_question' -ValueOnly -ErrorAction SilentlyContinue) -contains $k) { '✓' } else { '-' }
|
|
265
|
+
$a = if ((Get-Variable -Name 'keys_approval' -ValueOnly -ErrorAction SilentlyContinue) -contains $k) { '✓' } else { '-' }
|
|
266
|
+
$f = if ((Get-Variable -Name 'keys_fail' -ValueOnly -ErrorAction SilentlyContinue) -contains $k) { '✓' } else { '-' }
|
|
267
|
+
$warn = if ($f -eq '-') { ' ← 失败时无此音色台词' } else { '' }
|
|
268
|
+
Write-Host (" {0,-26} {1,-6} {2,-6} {3,-6} {4}{5}" -f $k, '✓', $q, $a, $f, $warn)
|
|
269
|
+
}
|
|
270
|
+
Write-Host ""
|
|
271
|
+
Write-Host "说明:某池未覆盖该音色时,v0.3.1+ 会在**同池内换一个音色**(不会哑掉,也不会越池播错语义)。" -ForegroundColor DarkGray
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# ── 语音包管理:列出本地已装包 ──────────────────────────────────────────────
|
|
275
|
+
function Show-Packs {
|
|
276
|
+
$vl = Join-Path $pluginRoot 'sounds\voice-lines'
|
|
277
|
+
Write-Host "=== 已装语音包 ===" -ForegroundColor Cyan
|
|
278
|
+
|
|
279
|
+
if (-not (Test-Path -LiteralPath $vl)) {
|
|
280
|
+
Write-Host " 没有任何语音包。" -ForegroundColor DarkGray
|
|
281
|
+
Write-Host " → 通知会使用系统提示音 + Toast(这是正常状态,不是错误)。" -ForegroundColor DarkGray
|
|
282
|
+
Write-Host " → 用 -ListVoices 查看目录里有哪些音色 key 可选;用 -PurgeVoice <key> 删除。" -ForegroundColor DarkGray
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# 按音色 key 汇总(去 ~N 变体)
|
|
287
|
+
$acc = @{}
|
|
288
|
+
foreach ($pool in @('', 'question', 'approval', 'fail')) {
|
|
289
|
+
$d = if ($pool) { Join-Path $vl $pool } else { $vl }
|
|
290
|
+
if (-not (Test-Path -LiteralPath $d)) { continue }
|
|
291
|
+
Get-ChildItem -LiteralPath $d -File -Filter '*.wav' -ErrorAction SilentlyContinue | ForEach-Object {
|
|
292
|
+
$k = $_.BaseName -replace '~\d+$', ''
|
|
293
|
+
if (-not $acc.ContainsKey($k)) { $acc[$k] = @{ pools = @(); bytes = 0; files = 0 } }
|
|
294
|
+
$pname = if ($pool) { $pool } else { 'done' }
|
|
295
|
+
if ($acc[$k].pools -notcontains $pname) { $acc[$k].pools += $pname }
|
|
296
|
+
$acc[$k].bytes += $_.Length
|
|
297
|
+
$acc[$k].files++
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if ($acc.Count -eq 0) {
|
|
302
|
+
Write-Host " 目录存在但没有 wav 文件。" -ForegroundColor DarkGray
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
$totalMB = 0
|
|
307
|
+
Write-Host (" {0,-26} {1,-34} {2}" -f '音色 key', '覆盖的语句类', '体积')
|
|
308
|
+
Write-Host (" " + ("-" * 74))
|
|
309
|
+
foreach ($k in ($acc.Keys | Sort-Object)) {
|
|
310
|
+
$v = $acc[$k]
|
|
311
|
+
$mb = [math]::Round($v.bytes / 1MB, 2)
|
|
312
|
+
$totalMB += $mb
|
|
313
|
+
Write-Host (" {0,-26} {1,-34} {2,7:N2} MB" -f $k, ($v.pools -join ','), $mb)
|
|
314
|
+
}
|
|
315
|
+
Write-Host (" " + ("-" * 74))
|
|
316
|
+
Write-Host (" 合计 {0} 个音色 / {1:N2} MB" -f $acc.Count, $totalMB) -ForegroundColor Cyan
|
|
317
|
+
Write-Host ""
|
|
318
|
+
Write-Host " 说明:只装了 done 的音色,在失败时会**在同池内换音色**或回落提示音(不会播错语义)。" -ForegroundColor DarkGray
|
|
319
|
+
Write-Host " 完整语音包约 48 MB;核心包只需 1 个音色。删掉不用的用 -PurgeVoice <key>。" -ForegroundColor DarkGray
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# ── 语音包管理:删除某音色 ──────────────────────────────────────────────────
|
|
323
|
+
function Remove-VoicePack([string]$key) {
|
|
324
|
+
if (-not $key) { Write-Host " 需要 -PurgeVoice <音色key>" -ForegroundColor Red; return }
|
|
325
|
+
if ($key -notmatch '^\w[\w-]*$') {
|
|
326
|
+
Write-Host " 非法音色 key:$key" -ForegroundColor Red; return
|
|
327
|
+
}
|
|
328
|
+
$vl = Join-Path $pluginRoot 'sounds\voice-lines'
|
|
329
|
+
$targets = @()
|
|
330
|
+
foreach ($pool in @('', 'question', 'approval', 'fail')) {
|
|
331
|
+
$d = if ($pool) { Join-Path $vl $pool } else { $vl }
|
|
332
|
+
if (-not (Test-Path -LiteralPath $d)) { continue }
|
|
333
|
+
Get-ChildItem -LiteralPath $d -File -Filter '*.wav' -ErrorAction SilentlyContinue | Where-Object {
|
|
334
|
+
($_.BaseName -replace '~\d+$', '') -eq $key
|
|
335
|
+
} | ForEach-Object { $targets += $_ }
|
|
336
|
+
}
|
|
337
|
+
if ($targets.Count -eq 0) {
|
|
338
|
+
Write-Host " 未找到音色 '$key' 的任何 wav。" -ForegroundColor Yellow
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
$mb = [math]::Round(($targets | Measure-Object Length -Sum).Sum / 1MB, 2)
|
|
342
|
+
Write-Host (" 将删除 {0} 个文件 / {1:N2} MB(音色 {2})" -f $targets.Count, $mb, $key) -ForegroundColor Yellow
|
|
343
|
+
if ($DryRun) { Write-Host " [DryRun] 未删除" -ForegroundColor Yellow; return }
|
|
344
|
+
$n = 0
|
|
345
|
+
foreach ($t in $targets) {
|
|
346
|
+
try { Remove-Item -LiteralPath $t.FullName -Force; $n++ } catch { }
|
|
347
|
+
}
|
|
348
|
+
Write-Host (" 已删除 {0} 个文件。" -f $n) -ForegroundColor Green
|
|
349
|
+
if (-not (Test-Path -LiteralPath $vl) -or @(Get-ChildItem -LiteralPath $vl -Recurse -File -Filter '*.wav' -ErrorAction SilentlyContinue).Count -eq 0) {
|
|
350
|
+
Write-Host " 已无任何语音包 → 通知将使用系统提示音 + Toast(正常状态)。" -ForegroundColor DarkGray
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# ── 语音包管理:说明如何获取更多 ────────────────────────────────────────────
|
|
355
|
+
function Show-InstallHint {
|
|
356
|
+
Write-Host "=== 如何获得更多语音 ===" -ForegroundColor Cyan
|
|
357
|
+
Write-Host ""
|
|
358
|
+
Write-Host " 核心包只带 1 个日语音色(约 1 MB)。完整多音色包体积较大,故不随包发布。" -ForegroundColor Gray
|
|
359
|
+
Write-Host ""
|
|
360
|
+
Write-Host " A) 自己合成(推荐,零分发成本、可自选音色)" -ForegroundColor Green
|
|
361
|
+
Write-Host " 1. 安装 VOICEVOX(本地免费 TTS 引擎,约 1 GB)"
|
|
362
|
+
Write-Host " 2. 启动引擎(默认 http://127.0.0.1:50021)"
|
|
363
|
+
Write-Host " 3. 跑仓库里的合成脚本:"
|
|
364
|
+
Write-Host " (合成脚本尚未随包发布;音色下载功能开发中)" -ForegroundColor DarkGray
|
|
365
|
+
Write-Host " 临时方案:手动在 VOICEVOX 里合成,把 wav 放进下面的 B) 目录"
|
|
366
|
+
Write-Host ""
|
|
367
|
+
Write-Host " B) 从他人镜像下载(如提供)" -ForegroundColor Green
|
|
368
|
+
Write-Host " 把 wav 放进 <插件根>\sounds\voice-lines\ 及子目录:"
|
|
369
|
+
Write-Host " done -> sounds\voice-lines\<key>~N.wav"
|
|
370
|
+
Write-Host " question -> sounds\voice-lines\question\<key>~N.wav"
|
|
371
|
+
Write-Host " approval -> sounds\voice-lines\approval\<key>~N.wav"
|
|
372
|
+
Write-Host " fail -> sounds\voice-lines\fail\<key>~N.wav"
|
|
373
|
+
Write-Host ""
|
|
374
|
+
Write-Host " C) 不装任何语音包(也是完全可用的配置)" -ForegroundColor Green
|
|
375
|
+
Write-Host " 通知 = Windows Toast + 系统提示音。用 -Voice off 可只留 Toast。" -ForegroundColor DarkGray
|
|
376
|
+
Write-Host ""
|
|
377
|
+
Write-Host " 装好后用 -ListPacks 查看,用 -VoiceName <key> 选定音色。" -ForegroundColor Gray
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# ── 主逻辑 ──────────────────────────────────────────────────────────────────
|
|
381
|
+
if ($Show) { Show-Effective; return }
|
|
382
|
+
if ($ListVoices) { Show-Voices; return }
|
|
383
|
+
if ($ListPacks) { Show-Packs; return }
|
|
384
|
+
if ($InstallHint) { Show-InstallHint; return }
|
|
385
|
+
if ($PSBoundParameters.ContainsKey('PurgeVoice')) { Remove-VoicePack $PurgeVoice; return }
|
|
386
|
+
|
|
387
|
+
if ($Clear) {
|
|
388
|
+
if (-not (Test-Path -LiteralPath $configPath)) {
|
|
389
|
+
Write-Host " 覆盖层本来就不存在,无需清理。" -ForegroundColor DarkGray
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
if ($DryRun) { Write-Host " [DryRun] 将删除 $configPath" -ForegroundColor Yellow; return }
|
|
393
|
+
Remove-Item -LiteralPath $configPath -Force
|
|
394
|
+
Write-Host " 已删除覆盖层:$configPath" -ForegroundColor Green
|
|
395
|
+
Write-Host " 控制权已交还「设置页 / cordis.patch.yml」。" -ForegroundColor Green
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
# 组装变更
|
|
400
|
+
$cur = Read-ConfigDoc
|
|
401
|
+
if (-not $cur.ok) {
|
|
402
|
+
Write-Host " 拒绝写入:配置覆盖层已损坏($($cur.reason))" -ForegroundColor Red
|
|
403
|
+
Write-Host " 原文件保留在:$configPath" -ForegroundColor Red
|
|
404
|
+
Write-Host " 请先修复或删除它,再重试。" -ForegroundColor Red
|
|
405
|
+
exit 1
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
$vals = @{}
|
|
409
|
+
foreach ($k in $cur.values.Keys) { $vals[$k] = $cur.values[$k] }
|
|
410
|
+
$changed = @{}
|
|
411
|
+
|
|
412
|
+
if ($PSBoundParameters.ContainsKey('Voice')) {
|
|
413
|
+
$v = ($Voice -eq 'on')
|
|
414
|
+
$vals['voice'] = $v; $changed['voice'] = $v
|
|
415
|
+
}
|
|
416
|
+
if ($PSBoundParameters.ContainsKey('Volume')) {
|
|
417
|
+
$vals['volume'] = $Volume; $changed['volume'] = $Volume
|
|
418
|
+
}
|
|
419
|
+
if ($PSBoundParameters.ContainsKey('VoiceMode')) {
|
|
420
|
+
$vals['voiceMode'] = $VoiceMode; $changed['voiceMode'] = $VoiceMode
|
|
421
|
+
}
|
|
422
|
+
if ($PSBoundParameters.ContainsKey('VoiceName')) {
|
|
423
|
+
if ($VoiceName -notmatch '^\w[\w-]*$') {
|
|
424
|
+
Write-Host " 非法音色 key:$VoiceName(只允许字母数字下划线连字符)" -ForegroundColor Red
|
|
425
|
+
exit 1
|
|
426
|
+
}
|
|
427
|
+
$vals['voiceName'] = $VoiceName; $changed['voiceName'] = $VoiceName
|
|
428
|
+
}
|
|
429
|
+
foreach ($e in @($Enable)) {
|
|
430
|
+
if (-not $e) { continue }
|
|
431
|
+
foreach ($one in ($e -split '[,\s]+' | Where-Object { $_ })) {
|
|
432
|
+
$key = $KindMap[$one.ToLower()]
|
|
433
|
+
if (-not $key) {
|
|
434
|
+
Write-Host " 未知类别:$one(可用:$($KindMap.Keys -join ', '))" -ForegroundColor Red
|
|
435
|
+
exit 1
|
|
436
|
+
}
|
|
437
|
+
$vals[$key] = $true; $changed[$key] = $true
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
foreach ($d in @($Disable)) {
|
|
441
|
+
if (-not $d) { continue }
|
|
442
|
+
foreach ($one in ($d -split '[,\s]+' | Where-Object { $_ })) {
|
|
443
|
+
$key = $KindMap[$one.ToLower()]
|
|
444
|
+
if (-not $key) {
|
|
445
|
+
Write-Host " 未知类别:$one(可用:$($KindMap.Keys -join ', '))" -ForegroundColor Red
|
|
446
|
+
exit 1
|
|
447
|
+
}
|
|
448
|
+
$vals[$key] = $false; $changed[$key] = $false
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if ($changed.Count -eq 0) {
|
|
453
|
+
Write-Host " 没有指定任何变更。用 -Show 查看当前值,或 -? 看用法。" -ForegroundColor Yellow
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
Write-Host "=== 将要变更 ===" -ForegroundColor Cyan
|
|
458
|
+
$changed.GetEnumerator() | Sort-Object Name | ForEach-Object {
|
|
459
|
+
$old = if ($cur.values.ContainsKey($_.Key)) { $cur.values[$_.Key] } else { '(未设)' }
|
|
460
|
+
$desc = if ($KindDesc.ContainsKey($_.Key)) { $KindDesc[$_.Key] } else { '' }
|
|
461
|
+
Write-Host (" {0,-20} {1} -> {2} {3}" -f $_.Key, $old, $_.Value, $desc)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (-not (Write-ConfigDoc $vals)) { exit 1 }
|
|
465
|
+
|
|
466
|
+
if (-not $DryRun) {
|
|
467
|
+
Write-Host ""
|
|
468
|
+
Write-Host " ✓ 已保存。插件下次播报即生效,**无需重启**。" -ForegroundColor Green
|
|
469
|
+
if ($changed.ContainsKey('voice') -and $changed['voice'] -eq $false) {
|
|
470
|
+
Write-Host " 语音已关闭;Toast 仍会弹出。" -ForegroundColor DarkGray
|
|
471
|
+
}
|
|
472
|
+
}
|