dsh-done-sound 0.1.7 → 0.1.9
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 +36 -13
- package/assets/turn-error.wav +0 -0
- package/assets/turn-interrupt.wav +0 -0
- package/assets/turn-pending.wav +0 -0
- package/assets/turn-retry.wav +0 -0
- package/lib/client.js +220 -67
- package/lib/index.js +90 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# dsh-done-sound
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness Web GUI
|
|
3
|
+
DeepSeek Harness Web GUI 的对话音效插件。不同触发情况可以分别设置音频提醒,并为每种情况提供内置默认音效。
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- **五种场景独立配置**:正常完成、中断、出错、等待确认、自动重连成功各自使用独立音频,可分别选择、试听和清除。
|
|
8
|
+
- **内置默认音效**:即使没有上传任何文件,也会使用对应场景的默认音效:
|
|
9
|
+
- 正常完成:`turn-done.wav`
|
|
10
|
+
- 中断:`turn-interrupt.wav`
|
|
11
|
+
- 出错:`turn-error.wav`
|
|
12
|
+
- 等待确认:`turn-pending.wav`
|
|
13
|
+
- 自动重连成功:`turn-retry.wav`
|
|
14
|
+
- **音频格式**:支持 `mp3`、`wav`、`ogg`、`webm`、`m4a`、`aac`、`flac`,单个文件最大 10MB。
|
|
15
|
+
- **完成检测**:根据宿主 `turn/end` 事件的 reason 判断正常完成、中断、出错和超长截断,避免误触发;每轮只播放一次。
|
|
9
16
|
- **触发开关**:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
17
|
+
- `中断时也响`:手动停止生成时是否播放,默认关闭。
|
|
18
|
+
- `出错时也响`:对话以错误结束或超长截断时是否播放,默认开启。
|
|
19
|
+
- `等待确认提醒`:Agent 等待批准、提问或其他人工确认时是否播放,默认开启。
|
|
20
|
+
- `重连成功也响`:自动发送「继续」成功后是否播放专属音效,默认开启。
|
|
21
|
+
- `出错自动重连`:出错后是否自动发送「继续」重试,默认开启。
|
|
22
|
+
- **自动重连等待时间**:可设置 10-300 秒,按 5 秒步进;等待期间显示倒计时,若会话自行恢复则自动取消。
|
|
23
|
+
- **全局音量**:统一调节五种场景的播放音量。
|
|
24
|
+
- **版本检查**:设置卡片显示当前版本和更新状态。
|
|
25
|
+
- **日志与导出**:Host 和 Client 诊断日志保存到 profile 目录,可从设置卡片导出当天日志。
|
|
16
26
|
|
|
17
27
|
## 安装
|
|
18
28
|
|
|
@@ -28,10 +38,22 @@ dsh plugin --profile web add link:F:\0.AI-CodeProject\DSHProject\0.日常聊天\
|
|
|
28
38
|
|
|
29
39
|
安装后重启 `dsh web`,打开设置页即可看到「对话完成音效」卡片。
|
|
30
40
|
|
|
41
|
+
升级已有安装后,请重启 `dsh web` 使 Host 端更新生效。旧版本中保存的单个自定义音频会自动作为「正常完成」音保留,不会丢失。
|
|
42
|
+
|
|
43
|
+
## 配置位置
|
|
44
|
+
|
|
45
|
+
设置 → 对话完成音效中会显示五行独立音频配置。每行都可以选择文件、试听或清除当前自定义音频;清除后会恢复该场景的内置默认音效。
|
|
46
|
+
|
|
47
|
+
自定义音频文件保存在 profile 目录的 `.dsh-done-sound/` 中,配置保存在 DSH 设置中。Host 音频路由为:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
/dsh-done-sound/audio/<fileId>
|
|
51
|
+
```
|
|
52
|
+
|
|
31
53
|
## 架构
|
|
32
54
|
|
|
33
|
-
- **Host 半区**(`src/index.js` → 构建产物 `lib/index.js`):`dsh-done-sound` 设置作用域(`ctx.settings
|
|
34
|
-
- **Client 半区**(`lib/client.js`):`settings.section` 设置卡片,以 fetch 调用 HTTP API
|
|
55
|
+
- **Host 半区**(`src/index.js` → 构建产物 `lib/index.js`):`dsh-done-sound` 设置作用域(`ctx.settings`)、五场景音频文件存储、`webServer` 路由与同源 JSON API(`/dsh-done-sound/api/status|config|audio|clear`)、`dsh-done-sound` 命令。路由经 `ctx.inject(['webServer'], cb)` 延迟挂载;构建时由 esbuild 内联 `@deepseek-ai/schemastery`,产物自包含(link 安装时无需依赖 profile 的模块解析)。
|
|
56
|
+
- **Client 半区**(`lib/client.js`):`settings.section` 设置卡片,以 fetch 调用 HTTP API;`conversation.session.header.utilities` 完成检测器订阅会话状态,按场景选择音效并处理等待确认与自动重连倒计时。
|
|
35
57
|
|
|
36
58
|
## 开发
|
|
37
59
|
|
|
@@ -39,9 +61,10 @@ dsh plugin --profile web add link:F:\0.AI-CodeProject\DSHProject\0.日常聊天\
|
|
|
39
61
|
pnpm install # 安装 esbuild
|
|
40
62
|
pnpm run build # 打包 src/index.js -> lib/index.js(自带)
|
|
41
63
|
pnpm run check # 语法检查两个产物
|
|
64
|
+
npm test # Host API 与完成检测测试
|
|
42
65
|
```
|
|
43
66
|
|
|
44
|
-
修改
|
|
67
|
+
修改 Host 源码后重新 `pnpm run build`;Client 半区直接改 `lib/client.js`(`__ModuleLoader__` 格式即运行时契约,无需打包)。
|
|
45
68
|
|
|
46
69
|
## 音效库下载
|
|
47
70
|
|
|
@@ -50,7 +73,7 @@ pnpm run check # 语法检查两个产物
|
|
|
50
73
|
- 链接:https://pan.baidu.com/s/1xnef4xkCy8pkooXMGi2ApQ?pwd=1234
|
|
51
74
|
- 提取码:1234
|
|
52
75
|
|
|
53
|
-
下载解压后,在 设置 → 对话完成音效 → 选择音频 中选用即可。
|
|
76
|
+
下载解压后,在 设置 → 对话完成音效 → 对应场景 → 选择音频 中选用即可。
|
|
54
77
|
|
|
55
78
|
## License
|
|
56
79
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -28,6 +28,7 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
'.dtc-row{justify-content:space-between;align-items:center;gap:12px;display:flex}',
|
|
29
29
|
'.dtc-muted{color:var(--dsw-alias-label-secondary);font-size:12px}',
|
|
30
30
|
'.dtc-err{color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;font-size:12px}',
|
|
31
|
+
'.dtc-ok{color:var(--dsw-alias-state-success-primary);white-space:pre-wrap;font-size:12px;word-break:break-all}',
|
|
31
32
|
'.dtc-btn{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border-radius:6px;padding:3px 12px;font-size:12px;white-space:nowrap}',
|
|
32
33
|
'.dtc-btn:disabled{opacity:.5;cursor:default}',
|
|
33
34
|
'.dtc-file{display:inline-flex;align-items:center}',
|
|
@@ -86,6 +87,19 @@ window.__ModuleLoader__.load({
|
|
|
86
87
|
retryFailed: '自动发送「继续」失败',
|
|
87
88
|
logPath: '日志文件',
|
|
88
89
|
exportLog: '导出日志',
|
|
90
|
+
exportSaved: '日志已导出',
|
|
91
|
+
exportSavedFile: '保存文件名:{name}',
|
|
92
|
+
exportSavedFallbackHint: '未弹出保存窗口,文件已存到浏览器默认下载文件夹(通常 C:\\Users\\你的用户名\\Downloads)',
|
|
93
|
+
exportLogPath: '原始日志路径:{path}(复制到资源管理器地址栏可打开)',
|
|
94
|
+
exportLogPathUnknown: '原始日志路径未知(可在插件目录 logs 文件夹查看)',
|
|
95
|
+
sceneNormal: '正常完成音',
|
|
96
|
+
sceneInterrupt: '中断音',
|
|
97
|
+
sceneError: '出错音',
|
|
98
|
+
scenePending: '等待确认音',
|
|
99
|
+
sceneRetry: '自动重连成功音',
|
|
100
|
+
defaultSound: '(默认音)',
|
|
101
|
+
retrySound: '重连成功也响',
|
|
102
|
+
retrySoundHint: '自动发送「继续」成功后播放提示音(默认开启)',
|
|
89
103
|
version: '当前版本',
|
|
90
104
|
updateAvailable: '有新版本',
|
|
91
105
|
upToDate: '已是最新',
|
|
@@ -134,6 +148,19 @@ window.__ModuleLoader__.load({
|
|
|
134
148
|
retryFailed: 'failed to auto-send "continue"',
|
|
135
149
|
logPath: 'Log file',
|
|
136
150
|
exportLog: 'Export log',
|
|
151
|
+
exportSaved: 'Log exported',
|
|
152
|
+
exportSavedFile: 'Saved file name: {name}',
|
|
153
|
+
exportSavedFallbackHint: 'No save dialog shown; saved to the browser default Downloads folder (usually C:\\Users\\YourName\\Downloads)',
|
|
154
|
+
exportLogPath: 'Original log path: {path} (copy into File Explorer address bar to open)',
|
|
155
|
+
exportLogPathUnknown: 'Original log path unknown (check the plugin logs folder)',
|
|
156
|
+
sceneNormal: 'Completion sound',
|
|
157
|
+
sceneInterrupt: 'Interrupt sound',
|
|
158
|
+
sceneError: 'Error sound',
|
|
159
|
+
scenePending: 'Approval-wait sound',
|
|
160
|
+
sceneRetry: 'Auto-retry success sound',
|
|
161
|
+
defaultSound: '(default)',
|
|
162
|
+
retrySound: 'Play on retry success',
|
|
163
|
+
retrySoundHint: 'Play a sound when "continue" is sent successfully (default on)',
|
|
137
164
|
version: 'Version',
|
|
138
165
|
updateAvailable: 'Update available',
|
|
139
166
|
upToDate: 'Up to date',
|
|
@@ -187,6 +214,29 @@ window.__ModuleLoader__.load({
|
|
|
187
214
|
}
|
|
188
215
|
|
|
189
216
|
// ---- tiny shared config store (card writes, detector reads) ----
|
|
217
|
+
const SCENES = ['normal', 'interrupt', 'error', 'pending', 'retry'];
|
|
218
|
+
const emptySounds = () =>
|
|
219
|
+
Object.fromEntries(
|
|
220
|
+
SCENES.map((s) => [s, { fileId: '', fileName: '', mime: '', size: 0, url: null, defaultUrl: null }]),
|
|
221
|
+
);
|
|
222
|
+
// Build the per-scene sounds map from a host status payload (reused by
|
|
223
|
+
// syncConfig and by the detector's fresh-view re-fetches).
|
|
224
|
+
const syncSoundsFrom = (payload, fallback) => {
|
|
225
|
+
const out = (fallback && fallback.sounds && typeof fallback.sounds === 'object' ? { ...fallback.sounds } : emptySounds());
|
|
226
|
+
const rawSounds = payload.sounds && typeof payload.sounds === 'object' ? payload.sounds : {};
|
|
227
|
+
for (const scene of SCENES) {
|
|
228
|
+
const s = rawSounds[scene] && typeof rawSounds[scene] === 'object' ? rawSounds[scene] : {};
|
|
229
|
+
out[scene] = {
|
|
230
|
+
fileId: typeof s.fileId === 'string' ? s.fileId : '',
|
|
231
|
+
fileName: typeof s.fileName === 'string' ? s.fileName : '',
|
|
232
|
+
mime: typeof s.mime === 'string' ? s.mime : '',
|
|
233
|
+
size: typeof s.size === 'number' ? s.size : 0,
|
|
234
|
+
url: typeof s.url === 'string' && s.url ? s.url : null,
|
|
235
|
+
defaultUrl: typeof s.defaultUrl === 'string' && s.defaultUrl ? s.defaultUrl : null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
};
|
|
190
240
|
const configStore = {
|
|
191
241
|
value: {
|
|
192
242
|
enabled: true,
|
|
@@ -194,17 +244,14 @@ window.__ModuleLoader__.load({
|
|
|
194
244
|
playOnInterrupt: false,
|
|
195
245
|
playOnError: true,
|
|
196
246
|
playOnPending: true,
|
|
247
|
+
playOnRetry: true,
|
|
197
248
|
autoRetryOnError: true,
|
|
198
249
|
retryDelaySeconds: 60,
|
|
199
250
|
version: null,
|
|
200
251
|
latestVersion: null,
|
|
201
252
|
updateStatus: null, // 'update' | 'uptodate' | 'error' | null
|
|
202
253
|
retryCooldownUntil: null, // timestamp until which auto-retry is cooling down
|
|
203
|
-
|
|
204
|
-
defaultUrl: null,
|
|
205
|
-
fileName: '',
|
|
206
|
-
mime: '',
|
|
207
|
-
size: 0,
|
|
254
|
+
sounds: emptySounds(),
|
|
208
255
|
loaded: false,
|
|
209
256
|
lastEvent: null,
|
|
210
257
|
},
|
|
@@ -259,8 +306,9 @@ window.__ModuleLoader__.load({
|
|
|
259
306
|
}
|
|
260
307
|
}
|
|
261
308
|
|
|
262
|
-
function playChime(cfg) {
|
|
263
|
-
const
|
|
309
|
+
function playChime(cfg, scene) {
|
|
310
|
+
const s = (cfg.sounds && cfg.sounds[scene]) || {};
|
|
311
|
+
const url = s.url || s.defaultUrl;
|
|
264
312
|
if (!cfg.enabled || !url) return;
|
|
265
313
|
try {
|
|
266
314
|
const audio = new Audio(url);
|
|
@@ -349,6 +397,13 @@ window.__ModuleLoader__.load({
|
|
|
349
397
|
} catch {
|
|
350
398
|
// ignore
|
|
351
399
|
}
|
|
400
|
+
// Play the dedicated "auto-retry ok" sound when enabled.
|
|
401
|
+
try {
|
|
402
|
+
const view = configStore.value;
|
|
403
|
+
if (view && view.enabled && view.playOnRetry !== false) playChime(view, 'retry');
|
|
404
|
+
} catch {
|
|
405
|
+
// never let a playback error break the retry flow
|
|
406
|
+
}
|
|
352
407
|
setConfig({
|
|
353
408
|
retryCooldownUntil: null,
|
|
354
409
|
lastEvent: { at: Date.now(), kind: 'error', reason: 'error', play: false, retried: true, retryFailed: false },
|
|
@@ -399,30 +454,38 @@ window.__ModuleLoader__.load({
|
|
|
399
454
|
pluginCtx = ctx;
|
|
400
455
|
const syncConfig = (payload) => {
|
|
401
456
|
if (!payload || typeof payload !== 'object') return;
|
|
457
|
+
const sounds = syncSoundsFrom(payload, null);
|
|
402
458
|
setConfig({
|
|
403
459
|
enabled: payload.enabled === true,
|
|
404
460
|
volume: typeof payload.volume === 'number' ? payload.volume : 0.8,
|
|
405
461
|
playOnInterrupt: payload.playOnInterrupt === true,
|
|
406
462
|
playOnError: payload.playOnError !== false,
|
|
407
463
|
playOnPending: payload.playOnPending !== false,
|
|
464
|
+
playOnRetry: payload.playOnRetry !== false,
|
|
408
465
|
autoRetryOnError: payload.autoRetryOnError !== false,
|
|
409
466
|
retryDelaySeconds: typeof payload.retryDelaySeconds === 'number' ? payload.retryDelaySeconds : 60,
|
|
410
467
|
version: typeof payload.version === 'string' ? payload.version : null,
|
|
411
468
|
logPath: typeof payload.logPath === 'string' && payload.logPath ? payload.logPath : null,
|
|
412
469
|
logFileName: typeof payload.logFileName === 'string' && payload.logFileName ? payload.logFileName : 'dsh-done-sound.log',
|
|
413
|
-
|
|
414
|
-
defaultUrl: typeof payload.defaultUrl === 'string' && payload.defaultUrl ? payload.defaultUrl : null,
|
|
415
|
-
fileName: payload.audio && typeof payload.audio.fileName === 'string' ? payload.audio.fileName : '',
|
|
416
|
-
mime: payload.audio && typeof payload.audio.mime === 'string' ? payload.audio.mime : '',
|
|
417
|
-
size: payload.audio && typeof payload.audio.size === 'number' ? payload.audio.size : 0,
|
|
470
|
+
sounds,
|
|
418
471
|
loaded: true,
|
|
419
472
|
});
|
|
420
473
|
};
|
|
474
|
+
// 预热:模块一激活就拉取 Host 持久化配置(如自动重连秒数),
|
|
475
|
+
// 避免页面刷新后倒计时/自动重连先用初始默认值(60s),
|
|
476
|
+
// 而设置卡却显示已保存值(20s)——两者必须一致。
|
|
477
|
+
// 失败静默,卡片挂载和 detector 挂载时还会再次同步兜底。
|
|
478
|
+
api('/dsh-done-sound/api/status')
|
|
479
|
+
.then((payload) => {
|
|
480
|
+
if (payload && typeof payload === 'object') syncConfig(payload);
|
|
481
|
+
})
|
|
482
|
+
.catch(() => {});
|
|
421
483
|
|
|
422
484
|
// ---- Settings card (global slot) ----
|
|
423
485
|
const TurnChimeCard = (props) => {
|
|
424
486
|
const cfg = useConfig();
|
|
425
487
|
const [error, setError] = react.useState(null);
|
|
488
|
+
const [exportInfo, setExportInfo] = react.useState(null);
|
|
426
489
|
const [busy, setBusy] = react.useState(false);
|
|
427
490
|
const [lang] = react.useState(resolveLang());
|
|
428
491
|
const L = (key) => t(lang, key);
|
|
@@ -471,8 +534,20 @@ window.__ModuleLoader__.load({
|
|
|
471
534
|
);
|
|
472
535
|
// Export today's log file: fetch it from the host, then hand the
|
|
473
536
|
// browser a Blob download — the native save dialog lets the user pick
|
|
474
|
-
// where to store it (filename is today's dated log name).
|
|
537
|
+
// where to store it (filename is today's dated log name). After a
|
|
538
|
+
// successful export, show a green banner with the saved file name and
|
|
539
|
+
// the original log path so the user can find the file on Windows even
|
|
540
|
+
// when the browser cannot show a save dialog.
|
|
475
541
|
const exportLog = () => {
|
|
542
|
+
setExportInfo(null);
|
|
543
|
+
const showSaved = (savedName, fallbackHint) => {
|
|
544
|
+
const lines = [L('exportSaved') + ' ✓', L('exportSavedFile').replace('{name}', savedName)];
|
|
545
|
+
if (fallbackHint) lines.push(L('exportSavedFallbackHint'));
|
|
546
|
+
// 原始日志路径来自 Host,复制到资源管理器地址栏即可直接打开,
|
|
547
|
+
// 最方便排查「有概率无音效」类问题。
|
|
548
|
+
lines.push(cfg.logPath ? L('exportLogPath').replace('{path}', cfg.logPath) : L('exportLogPathUnknown'));
|
|
549
|
+
setExportInfo(lines.join('\n'));
|
|
550
|
+
};
|
|
476
551
|
fetch('/dsh-done-sound/api/log/export')
|
|
477
552
|
.then((res) => {
|
|
478
553
|
if (!res.ok) {
|
|
@@ -483,15 +558,50 @@ window.__ModuleLoader__.load({
|
|
|
483
558
|
return res.text();
|
|
484
559
|
})
|
|
485
560
|
.then((text) => {
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
561
|
+
const fileName = cfg.logFileName || 'dsh-done-sound.log';
|
|
562
|
+
const fallbackDownload = () => {
|
|
563
|
+
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
|
564
|
+
const url = URL.createObjectURL(blob);
|
|
565
|
+
const a = document.createElement('a');
|
|
566
|
+
a.href = url;
|
|
567
|
+
a.download = fileName;
|
|
568
|
+
document.body.appendChild(a);
|
|
569
|
+
a.click();
|
|
570
|
+
a.remove();
|
|
571
|
+
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
|
572
|
+
showSaved(fileName, true);
|
|
573
|
+
};
|
|
574
|
+
// 优先用浏览器原生「另存为」对话框(File System Access API,
|
|
575
|
+
// Chrome/Edge 支持),让用户自己选择保存位置;
|
|
576
|
+
// 不支持或用户取消时回退为普通下载。
|
|
577
|
+
if (typeof window.showSaveFilePicker === 'function') {
|
|
578
|
+
window
|
|
579
|
+
.showSaveFilePicker({
|
|
580
|
+
suggestedName: fileName,
|
|
581
|
+
types: [{ description: '日志文件', accept: { 'text/plain': ['.log', '.txt'] } }],
|
|
582
|
+
})
|
|
583
|
+
.then((handle) => {
|
|
584
|
+
const savedName = handle && typeof handle.name === 'string' && handle.name ? handle.name : fileName;
|
|
585
|
+
return handle.createWritable().then(async (writable) => {
|
|
586
|
+
await writable.write(text);
|
|
587
|
+
await writable.close();
|
|
588
|
+
showSaved(savedName, false);
|
|
589
|
+
});
|
|
590
|
+
})
|
|
591
|
+
.catch((err) => {
|
|
592
|
+
// 用户点了「取消」关闭对话框 → 不算错误,直接结束。
|
|
593
|
+
if (err && err.name === 'AbortError') return;
|
|
594
|
+
// 其他失败(如手势过期、浏览器限制)→ 回退为普通下载。
|
|
595
|
+
try {
|
|
596
|
+
if (typeof console !== 'undefined') console.warn('[dsh-done-sound] save picker failed, falling back to download:', err);
|
|
597
|
+
} catch {
|
|
598
|
+
// ignore
|
|
599
|
+
}
|
|
600
|
+
fallbackDownload();
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
fallbackDownload();
|
|
495
605
|
})
|
|
496
606
|
.catch((err) => {
|
|
497
607
|
try {
|
|
@@ -559,7 +669,7 @@ window.__ModuleLoader__.load({
|
|
|
559
669
|
}
|
|
560
670
|
};
|
|
561
671
|
|
|
562
|
-
const onPick = (event) => {
|
|
672
|
+
const onPick = (event, scene) => {
|
|
563
673
|
const file = event.target.files && event.target.files[0];
|
|
564
674
|
if (event.target) event.target.value = '';
|
|
565
675
|
if (!file) return;
|
|
@@ -570,7 +680,7 @@ window.__ModuleLoader__.load({
|
|
|
570
680
|
setError(L('readFailed'));
|
|
571
681
|
return;
|
|
572
682
|
}
|
|
573
|
-
await run('/dsh-done-sound/api/audio', { dataUrl, fileName: file.name });
|
|
683
|
+
await run('/dsh-done-sound/api/audio', { dataUrl, fileName: file.name, scene });
|
|
574
684
|
};
|
|
575
685
|
reader.onerror = () => setError(L('readFailed'));
|
|
576
686
|
reader.readAsDataURL(file);
|
|
@@ -601,6 +711,34 @@ window.__ModuleLoader__.load({
|
|
|
601
711
|
if (volumeTimerRef.current) clearTimeout(volumeTimerRef.current);
|
|
602
712
|
}, []);
|
|
603
713
|
|
|
714
|
+
// One audio picker row per trigger scene. Each scene has its own
|
|
715
|
+
// uploaded file (or falls back to its bundled default sound).
|
|
716
|
+
const sceneRow = (scene) => {
|
|
717
|
+
const s = (cfg.sounds && cfg.sounds[scene]) || {};
|
|
718
|
+
return react.createElement(
|
|
719
|
+
'div',
|
|
720
|
+
{ className: 'dtc-row' },
|
|
721
|
+
react.createElement('span', { className: 'dtc-muted' }, L('scene' + scene[0].toUpperCase() + scene.slice(1))),
|
|
722
|
+
react.createElement('span', { className: 'dtc-muted dtc-logpath' }, s.fileName || L('defaultSound')),
|
|
723
|
+
react.createElement(
|
|
724
|
+
'label',
|
|
725
|
+
{ className: 'dtc-btn dtc-file' },
|
|
726
|
+
react.createElement('input', { type: 'file', accept: 'audio/*', disabled: busy, onChange: (e) => onPick(e, scene) }),
|
|
727
|
+
L('pick'),
|
|
728
|
+
),
|
|
729
|
+
react.createElement(
|
|
730
|
+
'button',
|
|
731
|
+
{ className: 'dtc-btn', type: 'button', disabled: busy || !(s.url || s.defaultUrl), onClick: () => playChime(cfg, scene) },
|
|
732
|
+
L('preview'),
|
|
733
|
+
),
|
|
734
|
+
react.createElement(
|
|
735
|
+
'button',
|
|
736
|
+
{ className: 'dtc-btn', type: 'button', disabled: busy || !s.fileId, onClick: () => run('/dsh-done-sound/api/clear', { scene }) },
|
|
737
|
+
L('clear'),
|
|
738
|
+
),
|
|
739
|
+
);
|
|
740
|
+
};
|
|
741
|
+
|
|
604
742
|
return react.createElement(
|
|
605
743
|
'div',
|
|
606
744
|
{ className: 'dtc-card' },
|
|
@@ -658,32 +796,9 @@ window.__ModuleLoader__.load({
|
|
|
658
796
|
L('exportLog'),
|
|
659
797
|
),
|
|
660
798
|
),
|
|
799
|
+
exportInfo !== null && react.createElement('div', { className: 'dtc-ok' }, exportInfo),
|
|
661
800
|
|
|
662
|
-
|
|
663
|
-
'div',
|
|
664
|
-
{ className: 'dtc-row' },
|
|
665
|
-
react.createElement('span', { className: 'dtc-muted' }, L('audio') + ':' + (cfg.fileName || L('none')) + (cfg.size ? ' (' + formatSize(cfg.size) + ')' : '')),
|
|
666
|
-
),
|
|
667
|
-
react.createElement(
|
|
668
|
-
'div',
|
|
669
|
-
{ className: 'dtc-row' },
|
|
670
|
-
react.createElement(
|
|
671
|
-
'label',
|
|
672
|
-
{ className: 'dtc-btn dtc-file' },
|
|
673
|
-
react.createElement('input', { type: 'file', accept: 'audio/*', disabled: busy, onChange: onPick }),
|
|
674
|
-
L('pick'),
|
|
675
|
-
),
|
|
676
|
-
react.createElement(
|
|
677
|
-
'button',
|
|
678
|
-
{ className: 'dtc-btn', type: 'button', disabled: busy || !cfg.url, onClick: () => playChime(cfg) },
|
|
679
|
-
L('preview'),
|
|
680
|
-
),
|
|
681
|
-
react.createElement(
|
|
682
|
-
'button',
|
|
683
|
-
{ className: 'dtc-btn', type: 'button', disabled: busy || !cfg.url, onClick: () => run('/dsh-done-sound/api/clear', {}) },
|
|
684
|
-
L('clear'),
|
|
685
|
-
),
|
|
686
|
-
),
|
|
801
|
+
SCENES.map((scene) => sceneRow(scene)),
|
|
687
802
|
|
|
688
803
|
react.createElement(
|
|
689
804
|
'div',
|
|
@@ -755,6 +870,24 @@ window.__ModuleLoader__.load({
|
|
|
755
870
|
),
|
|
756
871
|
react.createElement('span', { className: 'dtc-muted' }, L('pendingHint')),
|
|
757
872
|
),
|
|
873
|
+
react.createElement(
|
|
874
|
+
'div',
|
|
875
|
+
{ className: 'dtc-row' },
|
|
876
|
+
react.createElement(
|
|
877
|
+
'label',
|
|
878
|
+
{ className: 'dtc-toggle' },
|
|
879
|
+
react.createElement('input', {
|
|
880
|
+
type: 'checkbox',
|
|
881
|
+
checked: cfg.playOnRetry,
|
|
882
|
+
disabled: busy,
|
|
883
|
+
onChange: (e) => {
|
|
884
|
+
run('/dsh-done-sound/api/config', { playOnRetry: e.target.checked });
|
|
885
|
+
},
|
|
886
|
+
}),
|
|
887
|
+
react.createElement('span', null, L('retrySound')),
|
|
888
|
+
),
|
|
889
|
+
react.createElement('span', { className: 'dtc-muted' }, L('retrySoundHint')),
|
|
890
|
+
),
|
|
758
891
|
react.createElement(
|
|
759
892
|
'div',
|
|
760
893
|
{ className: 'dtc-row' },
|
|
@@ -874,6 +1007,17 @@ window.__ModuleLoader__.load({
|
|
|
874
1007
|
const prevPendingKeysRef = react.useRef('');
|
|
875
1008
|
const seededRef = react.useRef(false);
|
|
876
1009
|
|
|
1010
|
+
// 兜底:挂载时若配置尚未从 Host 同步(如页面刚刷新、apply 预热未完成),
|
|
1011
|
+
// 主动拉取一次,确保自动重连倒计时使用用户保存的秒数而非初始默认值。
|
|
1012
|
+
react.useEffect(() => {
|
|
1013
|
+
if (configStore.value.loaded) return;
|
|
1014
|
+
api('/dsh-done-sound/api/status')
|
|
1015
|
+
.then((payload) => {
|
|
1016
|
+
if (payload && typeof payload === 'object') syncConfig(payload);
|
|
1017
|
+
})
|
|
1018
|
+
.catch(() => {});
|
|
1019
|
+
}, []);
|
|
1020
|
+
|
|
877
1021
|
const snap =
|
|
878
1022
|
typeof useSession === 'function'
|
|
879
1023
|
? useSession((s) => {
|
|
@@ -925,10 +1069,13 @@ window.__ModuleLoader__.load({
|
|
|
925
1069
|
// or the audio was changed elsewhere), re-fetch status once and play
|
|
926
1070
|
// with the fresh view instead of silently skipping.
|
|
927
1071
|
const notifyPending = () => {
|
|
928
|
-
const canPlay = (view) =>
|
|
1072
|
+
const canPlay = (view) => {
|
|
1073
|
+
const s = (view.sounds && view.sounds.pending) || {};
|
|
1074
|
+
return view.enabled && view.playOnPending && (s.url || s.defaultUrl);
|
|
1075
|
+
};
|
|
929
1076
|
const record = (value) => setConfig({ lastEvent: { at: Date.now(), kind: 'pending', reason: 'pending', play: value } });
|
|
930
1077
|
if (canPlay(cfg)) {
|
|
931
|
-
playChime(cfg);
|
|
1078
|
+
playChime(cfg, 'pending');
|
|
932
1079
|
record(true);
|
|
933
1080
|
return;
|
|
934
1081
|
}
|
|
@@ -940,11 +1087,10 @@ window.__ModuleLoader__.load({
|
|
|
940
1087
|
...cfg,
|
|
941
1088
|
enabled: payload.enabled === true,
|
|
942
1089
|
playOnPending: payload.playOnPending !== false,
|
|
943
|
-
|
|
944
|
-
defaultUrl: typeof payload.defaultUrl === 'string' && payload.defaultUrl ? payload.defaultUrl : null,
|
|
1090
|
+
sounds: syncSoundsFrom(payload, cfg),
|
|
945
1091
|
};
|
|
946
1092
|
if (canPlay(fresh)) {
|
|
947
|
-
playChime(fresh);
|
|
1093
|
+
playChime(fresh, 'pending');
|
|
948
1094
|
record(true);
|
|
949
1095
|
}
|
|
950
1096
|
})
|
|
@@ -1031,33 +1177,40 @@ if (snap.pendingCount > 0) {
|
|
|
1031
1177
|
else kind = 'normal'; // 'completed' (and unknown future reasons default to playing)
|
|
1032
1178
|
|
|
1033
1179
|
// Fire the chime for a given config view; returns whether it played.
|
|
1034
|
-
|
|
1180
|
+
// Each trigger kind maps to its own per-scene audio (normal /
|
|
1181
|
+
// interrupt / error); pending and retry scenes are handled by their
|
|
1182
|
+
// own paths (notifyPending / fireModuleRetry success).
|
|
1035
1183
|
const firePlay = (view) => {
|
|
1036
1184
|
if (kind === 'interrupt') {
|
|
1037
|
-
if (view.playOnInterrupt
|
|
1038
|
-
playChime(view);
|
|
1185
|
+
if (view.playOnInterrupt) {
|
|
1186
|
+
playChime(view, 'interrupt');
|
|
1039
1187
|
return true;
|
|
1040
1188
|
}
|
|
1041
1189
|
return false;
|
|
1042
1190
|
}
|
|
1043
1191
|
if (kind === 'error') {
|
|
1044
|
-
if (view.playOnError
|
|
1045
|
-
playChime(view);
|
|
1192
|
+
if (view.playOnError) {
|
|
1193
|
+
playChime(view, 'error');
|
|
1046
1194
|
return true;
|
|
1047
1195
|
}
|
|
1048
1196
|
return false;
|
|
1049
1197
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
return
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1198
|
+
const normalSound = (view) => {
|
|
1199
|
+
const s = (view.sounds && view.sounds.normal) || {};
|
|
1200
|
+
return s.url || s.defaultUrl;
|
|
1201
|
+
};
|
|
1202
|
+
playChime(view, 'normal');
|
|
1203
|
+
return normalSound(view) ? true : false;
|
|
1055
1204
|
};
|
|
1056
1205
|
|
|
1057
1206
|
let played = firePlay(cfg);
|
|
1058
1207
|
const record = (value) =>
|
|
1059
1208
|
setConfig({ lastEvent: { at: Date.now(), kind, reason, play: value } });
|
|
1060
|
-
|
|
1209
|
+
const normalSoundMissing = () => {
|
|
1210
|
+
const s = (cfg.sounds && cfg.sounds.normal) || {};
|
|
1211
|
+
return !s.url && !s.defaultUrl;
|
|
1212
|
+
};
|
|
1213
|
+
if (!played && normalSoundMissing()) {
|
|
1061
1214
|
// Self-heal: the in-memory config may be stale (e.g. the audio was
|
|
1062
1215
|
// uploaded after this page mounted, or in another tab). Re-fetch
|
|
1063
1216
|
// status once and retry before giving up.
|
|
@@ -1072,8 +1225,8 @@ if (snap.pendingCount > 0) {
|
|
|
1072
1225
|
playOnInterrupt: payload.playOnInterrupt === true,
|
|
1073
1226
|
playOnError: payload.playOnError !== false,
|
|
1074
1227
|
playOnPending: payload.playOnPending !== false,
|
|
1075
|
-
|
|
1076
|
-
|
|
1228
|
+
playOnRetry: payload.playOnRetry !== false,
|
|
1229
|
+
sounds: syncSoundsFrom(payload, cfg),
|
|
1077
1230
|
};
|
|
1078
1231
|
record(firePlay(fresh));
|
|
1079
1232
|
} else {
|
package/lib/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { mkdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
|
5
5
|
import { join, dirname } from "node:path";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
9
9
|
function isNullable(value) {
|
|
10
10
|
return value === null || value === void 0;
|
|
11
11
|
}
|
|
@@ -191,7 +191,7 @@ var Time;
|
|
|
191
191
|
Time2.template = template;
|
|
192
192
|
})(Time || (Time = {}));
|
|
193
193
|
|
|
194
|
-
//
|
|
194
|
+
// node_modules/@deepseek-ai/schemastery/lib/index.mjs
|
|
195
195
|
var kSchema = Symbol.for("schemastery");
|
|
196
196
|
var kValidationError = Symbol.for("ValidationError");
|
|
197
197
|
globalThis.__schemastery_index__ ??= 0;
|
|
@@ -830,8 +830,21 @@ function writeLog(level, message) {
|
|
|
830
830
|
}
|
|
831
831
|
}
|
|
832
832
|
var MAX_BYTES = 10 * 1024 * 1024;
|
|
833
|
-
var
|
|
834
|
-
var
|
|
833
|
+
var SCENES = ["normal", "interrupt", "error", "pending", "retry"];
|
|
834
|
+
var DEFAULT_AUDIO_FILES = {
|
|
835
|
+
normal: "turn-done.wav",
|
|
836
|
+
interrupt: "turn-interrupt.wav",
|
|
837
|
+
error: "turn-error.wav",
|
|
838
|
+
pending: "turn-pending.wav",
|
|
839
|
+
retry: "turn-retry.wav"
|
|
840
|
+
};
|
|
841
|
+
var DEFAULT_AUDIO_PATHS = Object.fromEntries(
|
|
842
|
+
Object.entries(DEFAULT_AUDIO_FILES).map(([scene, file]) => [
|
|
843
|
+
scene,
|
|
844
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "assets", file)
|
|
845
|
+
])
|
|
846
|
+
);
|
|
847
|
+
var DEFAULT_AUDIO_URLS = Object.fromEntries(SCENES.map((s) => [s, `/dsh-done-sound/audio/default-${s}`]));
|
|
835
848
|
var DEFAULT_AUDIO_MIME = "audio/wav";
|
|
836
849
|
var AUDIO_MIMES = {
|
|
837
850
|
"audio/mpeg": "mp3",
|
|
@@ -848,14 +861,32 @@ var AUDIO_MIMES = {
|
|
|
848
861
|
"audio/flac": "flac",
|
|
849
862
|
"audio/x-flac": "flac"
|
|
850
863
|
};
|
|
864
|
+
var SoundMetaSchema = Schema.object({
|
|
865
|
+
fileId: Schema.string().default(""),
|
|
866
|
+
fileName: Schema.string().default(""),
|
|
867
|
+
mime: Schema.string().default(""),
|
|
868
|
+
size: Schema.number().default(0)
|
|
869
|
+
}).default({});
|
|
851
870
|
var TurnChimeSchema = Schema.object({
|
|
852
871
|
enabled: Schema.boolean().default(true),
|
|
853
872
|
volume: Schema.number().min(0).max(1).step(0.01).default(0.8),
|
|
854
873
|
playOnInterrupt: Schema.boolean().default(false),
|
|
855
874
|
playOnError: Schema.boolean().default(true),
|
|
856
875
|
playOnPending: Schema.boolean().default(true),
|
|
876
|
+
playOnRetry: Schema.boolean().default(true),
|
|
857
877
|
autoRetryOnError: Schema.boolean().default(true),
|
|
858
878
|
retryDelaySeconds: Schema.number().min(10).max(300).step(5).default(60),
|
|
879
|
+
// One independently configurable audio per trigger scene.
|
|
880
|
+
sounds: Schema.object({
|
|
881
|
+
normal: SoundMetaSchema,
|
|
882
|
+
interrupt: SoundMetaSchema,
|
|
883
|
+
error: SoundMetaSchema,
|
|
884
|
+
pending: SoundMetaSchema,
|
|
885
|
+
retry: SoundMetaSchema
|
|
886
|
+
}).default({}),
|
|
887
|
+
// Legacy single-audio field (pre-0.1.9). Kept for migration: when
|
|
888
|
+
// `sounds.normal` is empty, the old `audio` value is used for the normal
|
|
889
|
+
// scene until the user picks a new one.
|
|
859
890
|
audio: Schema.object({
|
|
860
891
|
fileId: Schema.string().default(""),
|
|
861
892
|
fileName: Schema.string().default(""),
|
|
@@ -871,9 +902,30 @@ function apply(ctx) {
|
|
|
871
902
|
const dataDir = join(profileDir, ".dsh-done-sound");
|
|
872
903
|
const scope = ctx.settings.register(NS, TurnChimeSchema, { base: {} });
|
|
873
904
|
const audioPath = (fileId) => join(dataDir, `${fileId}.bin`);
|
|
905
|
+
const isScene = (s) => typeof s === "string" && SCENES.includes(s);
|
|
906
|
+
const sceneAudio = (value, scene) => {
|
|
907
|
+
const s = value.sounds?.[scene];
|
|
908
|
+
if (s && typeof s.fileId === "string" && s.fileId) return s;
|
|
909
|
+
if (scene === "normal" && value.audio && typeof value.audio.fileId === "string" && value.audio.fileId) {
|
|
910
|
+
return value.audio;
|
|
911
|
+
}
|
|
912
|
+
return { fileId: "", fileName: "", mime: "", size: 0 };
|
|
913
|
+
};
|
|
874
914
|
const statusPayload = () => {
|
|
875
915
|
const value = scope.get();
|
|
876
|
-
const
|
|
916
|
+
const sounds = {};
|
|
917
|
+
for (const scene of SCENES) {
|
|
918
|
+
const meta = sceneAudio(value, scene);
|
|
919
|
+
const fileId = meta.fileId ?? "";
|
|
920
|
+
sounds[scene] = {
|
|
921
|
+
fileId,
|
|
922
|
+
fileName: meta.fileName ?? "",
|
|
923
|
+
mime: meta.mime ?? "",
|
|
924
|
+
size: meta.size ?? 0,
|
|
925
|
+
url: fileId ? `${ROUTE_PREFIX}/audio/${fileId}` : null,
|
|
926
|
+
defaultUrl: DEFAULT_AUDIO_URLS[scene]
|
|
927
|
+
};
|
|
928
|
+
}
|
|
877
929
|
return {
|
|
878
930
|
ok: true,
|
|
879
931
|
version: pluginVersion(),
|
|
@@ -882,33 +934,31 @@ function apply(ctx) {
|
|
|
882
934
|
playOnInterrupt: value.playOnInterrupt,
|
|
883
935
|
playOnError: value.playOnError,
|
|
884
936
|
playOnPending: value.playOnPending,
|
|
937
|
+
playOnRetry: value.playOnRetry,
|
|
885
938
|
autoRetryOnError: value.autoRetryOnError,
|
|
886
939
|
retryDelaySeconds: value.retryDelaySeconds,
|
|
887
|
-
|
|
888
|
-
fileId,
|
|
889
|
-
fileName: value.audio?.fileName ?? "",
|
|
890
|
-
mime: value.audio?.mime ?? "",
|
|
891
|
-
size: value.audio?.size ?? 0
|
|
892
|
-
},
|
|
893
|
-
url: fileId ? `${ROUTE_PREFIX}/audio/${fileId}` : null,
|
|
894
|
-
defaultUrl: DEFAULT_AUDIO_URL,
|
|
940
|
+
sounds,
|
|
895
941
|
logPath: datedLogPath(),
|
|
896
942
|
logFileName: datedLogFileName()
|
|
897
943
|
};
|
|
898
944
|
};
|
|
899
|
-
const clearAudio = async () => {
|
|
945
|
+
const clearAudio = async (scene) => {
|
|
900
946
|
const value = scope.get();
|
|
901
|
-
const
|
|
947
|
+
const target = isScene(scene) ? scene : "normal";
|
|
948
|
+
const meta = sceneAudio(value, target);
|
|
949
|
+
const fileId = meta.fileId;
|
|
902
950
|
if (fileId) {
|
|
903
951
|
try {
|
|
904
952
|
await unlink(audioPath(fileId));
|
|
905
953
|
} catch {
|
|
906
954
|
}
|
|
907
955
|
}
|
|
908
|
-
|
|
909
|
-
|
|
956
|
+
const patch = { sounds: { ...value.sounds ?? {}, [target]: { fileId: "", fileName: "", mime: "", size: 0 } } };
|
|
957
|
+
if (target === "normal") patch.audio = { fileId: "", fileName: "", mime: "", size: 0 };
|
|
958
|
+
await scope.update(patch);
|
|
959
|
+
writeLog("INFO", `audio cleared (${target})`);
|
|
910
960
|
};
|
|
911
|
-
const setAudio = async (dataUrl, fileName) => {
|
|
961
|
+
const setAudio = async (dataUrl, fileName, scene) => {
|
|
912
962
|
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/.exec(dataUrl);
|
|
913
963
|
if (!match) throw new Error("invalid audio data (need data:<mime>;base64,<data>)");
|
|
914
964
|
const mime = match[1].toLowerCase();
|
|
@@ -917,20 +967,23 @@ function apply(ctx) {
|
|
|
917
967
|
const buffer = Buffer.from(match[2], "base64");
|
|
918
968
|
if (buffer.length === 0) throw new Error("empty audio content");
|
|
919
969
|
if (buffer.length > MAX_BYTES) throw new Error(`audio exceeds 10MB (got ${buffer.length} bytes)`);
|
|
970
|
+
const target = isScene(scene) ? scene : "normal";
|
|
920
971
|
const fileId = randomUUID();
|
|
921
972
|
await mkdir(dataDir, { recursive: true });
|
|
922
973
|
await writeFile(audioPath(fileId), buffer);
|
|
923
|
-
const previous = scope.get().
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
974
|
+
const previous = sceneAudio(scope.get(), target).fileId;
|
|
975
|
+
const patch = {
|
|
976
|
+
sounds: { ...scope.get().sounds ?? {}, [target]: { fileId, fileName: fileName || "chime", mime, size: buffer.length } }
|
|
977
|
+
};
|
|
978
|
+
if (target === "normal") patch.audio = { fileId, fileName: fileName || "chime", mime, size: buffer.length };
|
|
979
|
+
await scope.update(patch);
|
|
927
980
|
if (previous && previous !== fileId) {
|
|
928
981
|
try {
|
|
929
982
|
await unlink(audioPath(previous));
|
|
930
983
|
} catch {
|
|
931
984
|
}
|
|
932
985
|
}
|
|
933
|
-
writeLog("INFO", `audio stored: ${fileName || "chime"} (${buffer.length} bytes, ${mime})`);
|
|
986
|
+
writeLog("INFO", `audio stored (${target}): ${fileName || "chime"} (${buffer.length} bytes, ${mime})`);
|
|
934
987
|
return fileId;
|
|
935
988
|
};
|
|
936
989
|
ctx.commands.register({
|
|
@@ -1022,9 +1075,11 @@ function apply(ctx) {
|
|
|
1022
1075
|
pathname = "/";
|
|
1023
1076
|
}
|
|
1024
1077
|
const method = (req.method ?? "GET").toUpperCase();
|
|
1025
|
-
|
|
1078
|
+
const defaultMatch = /^\/dsh-done-sound\/audio\/default-([A-Za-z0-9-]+)$/.exec(pathname);
|
|
1079
|
+
if (defaultMatch && SCENES.includes(defaultMatch[1])) {
|
|
1080
|
+
const scene = defaultMatch[1];
|
|
1026
1081
|
try {
|
|
1027
|
-
const buffer = await readFile(
|
|
1082
|
+
const buffer = await readFile(DEFAULT_AUDIO_PATHS[scene]);
|
|
1028
1083
|
res.writeHead(200, {
|
|
1029
1084
|
"Content-Type": DEFAULT_AUDIO_MIME,
|
|
1030
1085
|
"Content-Length": buffer.length,
|
|
@@ -1041,8 +1096,9 @@ function apply(ctx) {
|
|
|
1041
1096
|
const audioMatch = /^\/dsh-done-sound\/audio\/([A-Za-z0-9-]+)$/.exec(pathname);
|
|
1042
1097
|
if (audioMatch) {
|
|
1043
1098
|
const fileId = audioMatch[1];
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1099
|
+
const value = scope.get();
|
|
1100
|
+
const meta = SCENES.map((s) => sceneAudio(value, s)).find((m) => m.fileId === fileId);
|
|
1101
|
+
if (!FILE_ID_RE.test(fileId) || !meta) {
|
|
1046
1102
|
res.writeHead(404);
|
|
1047
1103
|
res.end("not found");
|
|
1048
1104
|
return;
|
|
@@ -1050,7 +1106,7 @@ function apply(ctx) {
|
|
|
1050
1106
|
try {
|
|
1051
1107
|
const buffer = await readFile(audioPath(fileId));
|
|
1052
1108
|
res.writeHead(200, {
|
|
1053
|
-
"Content-Type":
|
|
1109
|
+
"Content-Type": meta.mime || "application/octet-stream",
|
|
1054
1110
|
"Content-Length": buffer.length,
|
|
1055
1111
|
"Cache-Control": "no-cache",
|
|
1056
1112
|
"X-Content-Type-Options": "nosniff"
|
|
@@ -1075,6 +1131,7 @@ function apply(ctx) {
|
|
|
1075
1131
|
if (typeof body.playOnInterrupt === "boolean") patch.playOnInterrupt = body.playOnInterrupt;
|
|
1076
1132
|
if (typeof body.playOnError === "boolean") patch.playOnError = body.playOnError;
|
|
1077
1133
|
if (typeof body.playOnPending === "boolean") patch.playOnPending = body.playOnPending;
|
|
1134
|
+
if (typeof body.playOnRetry === "boolean") patch.playOnRetry = body.playOnRetry;
|
|
1078
1135
|
if (typeof body.autoRetryOnError === "boolean") patch.autoRetryOnError = body.autoRetryOnError;
|
|
1079
1136
|
if (typeof body.retryDelaySeconds === "number" && body.retryDelaySeconds >= 10 && body.retryDelaySeconds <= 300) {
|
|
1080
1137
|
patch.retryDelaySeconds = Math.round(body.retryDelaySeconds / 5) * 5;
|
|
@@ -1085,12 +1142,15 @@ function apply(ctx) {
|
|
|
1085
1142
|
}
|
|
1086
1143
|
if (pathname === "/dsh-done-sound/api/audio" && method === "POST") {
|
|
1087
1144
|
const body = await readJsonBody(req);
|
|
1088
|
-
|
|
1145
|
+
const scene = typeof body.scene === "string" && SCENES.includes(body.scene) ? body.scene : "normal";
|
|
1146
|
+
await setAudio(typeof body.dataUrl === "string" ? body.dataUrl : "", typeof body.fileName === "string" ? body.fileName : "", scene);
|
|
1089
1147
|
sendJson(res, 200, statusPayload());
|
|
1090
1148
|
return;
|
|
1091
1149
|
}
|
|
1092
1150
|
if (pathname === "/dsh-done-sound/api/clear" && method === "POST") {
|
|
1093
|
-
await
|
|
1151
|
+
const body = await readJsonBody(req);
|
|
1152
|
+
const scene = typeof body.scene === "string" && SCENES.includes(body.scene) ? body.scene : "normal";
|
|
1153
|
+
await clearAudio(scene);
|
|
1094
1154
|
sendJson(res, 200, statusPayload());
|
|
1095
1155
|
return;
|
|
1096
1156
|
}
|