dsh-done-sound 0.1.2 → 0.1.3

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.
Binary file
package/lib/client.js CHANGED
@@ -60,6 +60,8 @@ window.__ModuleLoader__.load({
60
60
  interruptHint: '手动停止生成时也播放(默认关闭)',
61
61
  error: '出错时也响',
62
62
  errorHint: '对话以错误结束时也播放(默认开启)',
63
+ pending: '等待确认时提醒',
64
+ pendingHint: 'Agent 需要你确认操作(如审批)而停住等待时播放提醒(默认开启)',
63
65
  saved: '已保存:',
64
66
  readFailed: '读取文件失败',
65
67
  apiFailed: '无法连接插件服务(Host 未加载?),请重启 dsh web',
@@ -69,6 +71,7 @@ window.__ModuleLoader__.load({
69
71
  evNormal: '正常完成',
70
72
  evInterrupt: '中断',
71
73
  evError: '出错',
74
+ evPending: '等待确认',
72
75
  evPlayed: '已播放',
73
76
  evNotPlayed: '未播放',
74
77
  },
@@ -86,6 +89,8 @@ window.__ModuleLoader__.load({
86
89
  interruptHint: 'Also play when generation is stopped (default off)',
87
90
  error: 'Play on error',
88
91
  errorHint: 'Also play when a turn ends in error (default on)',
92
+ pending: 'Alert on approval wait',
93
+ pendingHint: 'Play a reminder when the agent is waiting for your confirmation (default on)',
89
94
  saved: 'Saved:',
90
95
  readFailed: 'Failed to read file',
91
96
  apiFailed: 'Cannot reach the plugin host service (host not loaded?), restart dsh web',
@@ -95,6 +100,7 @@ window.__ModuleLoader__.load({
95
100
  evNormal: 'completed',
96
101
  evInterrupt: 'interrupted',
97
102
  evError: 'error',
103
+ evPending: 'approval wait',
98
104
  evPlayed: 'played',
99
105
  evNotPlayed: 'not played',
100
106
  },
@@ -130,7 +136,9 @@ window.__ModuleLoader__.load({
130
136
  volume: 0.8,
131
137
  playOnInterrupt: false,
132
138
  playOnError: true,
139
+ playOnPending: true,
133
140
  url: null,
141
+ defaultUrl: null,
134
142
  fileName: '',
135
143
  mime: '',
136
144
  size: 0,
@@ -159,9 +167,10 @@ window.__ModuleLoader__.load({
159
167
  return cfg;
160
168
  }
161
169
  function playChime(cfg) {
162
- if (!cfg.enabled || !cfg.url) return;
170
+ const url = cfg.url || cfg.defaultUrl;
171
+ if (!cfg.enabled || !url) return;
163
172
  try {
164
- const audio = new Audio(cfg.url);
173
+ const audio = new Audio(url);
165
174
  audio.volume = cfg.volume;
166
175
  audio.play().catch(() => {});
167
176
  } catch {
@@ -203,7 +212,9 @@ window.__ModuleLoader__.load({
203
212
  volume: typeof payload.volume === 'number' ? payload.volume : 0.8,
204
213
  playOnInterrupt: payload.playOnInterrupt === true,
205
214
  playOnError: payload.playOnError !== false,
215
+ playOnPending: payload.playOnPending !== false,
206
216
  url: typeof payload.url === 'string' && payload.url ? payload.url : null,
217
+ defaultUrl: typeof payload.defaultUrl === 'string' && payload.defaultUrl ? payload.defaultUrl : null,
207
218
  fileName: payload.audio && typeof payload.audio.fileName === 'string' ? payload.audio.fileName : '',
208
219
  mime: payload.audio && typeof payload.audio.mime === 'string' ? payload.audio.mime : '',
209
220
  size: payload.audio && typeof payload.audio.size === 'number' ? payload.audio.size : 0,
@@ -395,6 +406,24 @@ window.__ModuleLoader__.load({
395
406
  ),
396
407
  react.createElement('span', { className: 'dtc-muted' }, L('errorHint')),
397
408
  ),
409
+ react.createElement(
410
+ 'div',
411
+ { className: 'dtc-row' },
412
+ react.createElement(
413
+ 'label',
414
+ { className: 'dtc-toggle' },
415
+ react.createElement('input', {
416
+ type: 'checkbox',
417
+ checked: cfg.playOnPending,
418
+ disabled: busy,
419
+ onChange: (e) => {
420
+ run('/dsh-done-sound/api/config', { playOnPending: e.target.checked });
421
+ },
422
+ }),
423
+ react.createElement('span', null, L('pending')),
424
+ ),
425
+ react.createElement('span', { className: 'dtc-muted' }, L('pendingHint')),
426
+ ),
398
427
 
399
428
  error !== null && react.createElement('div', { className: 'dtc-err' }, error),
400
429
  !cfg.loaded && react.createElement('div', { className: 'dtc-muted' }, L('loading')),
@@ -406,7 +435,13 @@ window.__ModuleLoader__.load({
406
435
  ':' +
407
436
  formatEventTime(cfg.lastEvent.at) +
408
437
  ' ' +
409
- (cfg.lastEvent.kind === 'interrupt' ? L('evInterrupt') : cfg.lastEvent.kind === 'error' ? L('evError') : L('evNormal')) +
438
+ (cfg.lastEvent.kind === 'interrupt'
439
+ ? L('evInterrupt')
440
+ : cfg.lastEvent.kind === 'error'
441
+ ? L('evError')
442
+ : cfg.lastEvent.kind === 'pending'
443
+ ? L('evPending')
444
+ : L('evNormal')) +
410
445
  ' → ' +
411
446
  (cfg.lastEvent.play ? L('evPlayed') : L('evNotPlayed')),
412
447
  ),
@@ -426,6 +461,7 @@ window.__ModuleLoader__.load({
426
461
  const pendingReasonRef = react.useRef(null);
427
462
  const lastEndTurnRef = react.useRef(0);
428
463
  const lastSessionRef = react.useRef(null);
464
+ const prevPendingRef = react.useRef(false);
429
465
 
430
466
  const snap =
431
467
  typeof useSession === 'function'
@@ -447,6 +483,8 @@ window.__ModuleLoader__.load({
447
483
  streaming: s.partial !== null || s.running === true,
448
484
  endReason,
449
485
  endTurn,
486
+ // agent is waiting for human intervention (approval etc.)
487
+ pendingCount: s && Array.isArray(s.pending) ? s.pending.length : 0,
450
488
  };
451
489
  })
452
490
  : null;
@@ -460,8 +498,22 @@ window.__ModuleLoader__.load({
460
498
  lastSessionRef.current = snap.sessionId;
461
499
  pendingReasonRef.current = null;
462
500
  lastEndTurnRef.current = snap.endTurn;
501
+ prevPendingRef.current = snap.pendingCount > 0;
502
+ return;
503
+ }
504
+
505
+ // Agent entered a waiting-for-human state (rising edge only).
506
+ if (snap.pendingCount > 0 && !prevPendingRef.current) {
507
+ prevPendingRef.current = true;
508
+ let played = false;
509
+ if (cfg.enabled && cfg.playOnPending && (cfg.url || cfg.defaultUrl)) {
510
+ playChime(cfg);
511
+ played = true;
512
+ }
513
+ setConfig({ lastEvent: { at: Date.now(), kind: 'pending', reason: 'pending', play: played } });
463
514
  return;
464
515
  }
516
+ prevPendingRef.current = snap.pendingCount > 0;
465
517
 
466
518
  // A newly-closed turn is the authoritative "a turn ended" signal.
467
519
  if (snap.endTurn > lastEndTurnRef.current) {
@@ -480,24 +532,60 @@ window.__ModuleLoader__.load({
480
532
  else if (reason === 'error' || reason === 'max-tokens') kind = 'error';
481
533
  else kind = 'normal'; // 'completed' (and unknown future reasons default to playing)
482
534
 
483
- let played = false;
484
- if (kind === 'interrupt') {
485
- if (cfg.playOnInterrupt && cfg.url) {
486
- playChime(cfg);
487
- played = true;
535
+ // Fire the chime for a given config view; returns whether it played.
536
+ const effectiveUrlOf = (view) => view.url || view.defaultUrl;
537
+ const firePlay = (view) => {
538
+ if (kind === 'interrupt') {
539
+ if (view.playOnInterrupt && effectiveUrlOf(view)) {
540
+ playChime(view);
541
+ return true;
542
+ }
543
+ return false;
488
544
  }
489
- } else if (kind === 'error') {
490
- if (cfg.playOnError && cfg.url) {
491
- playChime(cfg);
492
- played = true;
545
+ if (kind === 'error') {
546
+ if (view.playOnError && effectiveUrlOf(view)) {
547
+ playChime(view);
548
+ return true;
549
+ }
550
+ return false;
493
551
  }
494
- } else if (cfg.url) {
495
- playChime(cfg);
496
- played = true;
497
- }
552
+ if (effectiveUrlOf(view)) {
553
+ playChime(view);
554
+ return true;
555
+ }
556
+ return false;
557
+ };
498
558
 
499
- // Visible diagnostic for the settings card (and future debugging).
500
- setConfig({ lastEvent: { at: Date.now(), kind, reason, play: played } });
559
+ let played = firePlay(cfg);
560
+ const record = (value) =>
561
+ setConfig({ lastEvent: { at: Date.now(), kind, reason, play: value } });
562
+ if (!played && !cfg.url && !cfg.defaultUrl) {
563
+ // Self-heal: the in-memory config may be stale (e.g. the audio was
564
+ // uploaded after this page mounted, or in another tab). Re-fetch
565
+ // status once and retry before giving up.
566
+ api('/dsh-done-sound/api/status')
567
+ .then((payload) => {
568
+ if (payload && typeof payload === 'object') {
569
+ syncConfig(payload);
570
+ const fresh = {
571
+ ...cfg,
572
+ enabled: payload.enabled === true,
573
+ volume: typeof payload.volume === 'number' ? payload.volume : 0.8,
574
+ playOnInterrupt: payload.playOnInterrupt === true,
575
+ playOnError: payload.playOnError !== false,
576
+ playOnPending: payload.playOnPending !== false,
577
+ url: typeof payload.url === 'string' && payload.url ? payload.url : null,
578
+ defaultUrl: typeof payload.defaultUrl === 'string' && payload.defaultUrl ? payload.defaultUrl : null,
579
+ };
580
+ record(firePlay(fresh));
581
+ } else {
582
+ record(false);
583
+ }
584
+ })
585
+ .catch(() => record(false));
586
+ } else {
587
+ record(played);
588
+ }
501
589
  }, [snap, cfg]);
502
590
 
503
591
  return null;
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // dsh-done-sound/src/index.js
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { mkdir, readFile, writeFile, unlink } from "node:fs/promises";
4
- import { join } from "node:path";
4
+ import { join, dirname } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
 
7
7
  // C:/Users/Administrator/.dsh/profiles/web/node_modules/@deepseek-ai/cosmokit/lib/index.js
@@ -796,6 +796,9 @@ defineMethod("transform", [
796
796
  var name = "dsh-done-sound";
797
797
  var inject = ["settings", "commands", "webServer"];
798
798
  var MAX_BYTES = 2 * 1024 * 1024;
799
+ var DEFAULT_AUDIO_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "turn-done.wav");
800
+ var DEFAULT_AUDIO_URL = "/dsh-done-sound/audio/default";
801
+ var DEFAULT_AUDIO_MIME = "audio/wav";
799
802
  var AUDIO_MIMES = {
800
803
  "audio/mpeg": "mp3",
801
804
  "audio/mp3": "mp3",
@@ -816,6 +819,7 @@ var TurnChimeSchema = Schema.object({
816
819
  volume: Schema.number().min(0).max(1).step(0.01).default(0.8),
817
820
  playOnInterrupt: Schema.boolean().default(false),
818
821
  playOnError: Schema.boolean().default(true),
822
+ playOnPending: Schema.boolean().default(true),
819
823
  audio: Schema.object({
820
824
  fileId: Schema.string().default(""),
821
825
  fileName: Schema.string().default(""),
@@ -840,13 +844,15 @@ function apply(ctx) {
840
844
  volume: value.volume,
841
845
  playOnInterrupt: value.playOnInterrupt,
842
846
  playOnError: value.playOnError,
847
+ playOnPending: value.playOnPending,
843
848
  audio: {
844
849
  fileId,
845
850
  fileName: value.audio?.fileName ?? "",
846
851
  mime: value.audio?.mime ?? "",
847
852
  size: value.audio?.size ?? 0
848
853
  },
849
- url: fileId ? `${ROUTE_PREFIX}/audio/${fileId}` : null
854
+ url: fileId ? `${ROUTE_PREFIX}/audio/${fileId}` : null,
855
+ defaultUrl: DEFAULT_AUDIO_URL
850
856
  };
851
857
  };
852
858
  const clearAudio = async () => {
@@ -972,6 +978,22 @@ function apply(ctx) {
972
978
  pathname = "/";
973
979
  }
974
980
  const method = (req.method ?? "GET").toUpperCase();
981
+ if (pathname === DEFAULT_AUDIO_URL) {
982
+ try {
983
+ const buffer = await readFile(DEFAULT_AUDIO_PATH);
984
+ res.writeHead(200, {
985
+ "Content-Type": DEFAULT_AUDIO_MIME,
986
+ "Content-Length": buffer.length,
987
+ "Cache-Control": "no-cache",
988
+ "X-Content-Type-Options": "nosniff"
989
+ });
990
+ res.end(buffer);
991
+ } catch {
992
+ res.writeHead(404);
993
+ res.end("not found");
994
+ }
995
+ return;
996
+ }
975
997
  const audioMatch = /^\/dsh-done-sound\/audio\/([A-Za-z0-9-]+)$/.exec(pathname);
976
998
  if (audioMatch) {
977
999
  const fileId = audioMatch[1];
@@ -1008,6 +1030,7 @@ function apply(ctx) {
1008
1030
  if (typeof body.volume === "number" && body.volume >= 0 && body.volume <= 1) patch.volume = body.volume;
1009
1031
  if (typeof body.playOnInterrupt === "boolean") patch.playOnInterrupt = body.playOnInterrupt;
1010
1032
  if (typeof body.playOnError === "boolean") patch.playOnError = body.playOnError;
1033
+ if (typeof body.playOnPending === "boolean") patch.playOnPending = body.playOnPending;
1011
1034
  if (Object.keys(patch).length > 0) await scope.update(patch);
1012
1035
  sendJson(res, 200, statusPayload());
1013
1036
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-done-sound",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Play a user-chosen sound whenever a conversation finishes in the DeepSeek Harness web GUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "files": [
32
32
  "lib",
33
+ "assets",
33
34
  "cordis.patch.yml",
34
35
  "README.md",
35
36
  "LICENSE"
@@ -81,8 +82,12 @@
81
82
  "devDependencies": {
82
83
  "esbuild": "^0.25.0"
83
84
  },
85
+ "engines": {
86
+ "node": ">=18"
87
+ },
84
88
  "scripts": {
85
89
  "build": "esbuild src/index.js --bundle --format=esm --platform=node --target=node22 --outfile=lib/index.js",
86
- "check": "node --check lib/index.js && node --check lib/client.js"
90
+ "check": "node --check lib/index.js && node --check lib/client.js",
91
+ "test": "node test/host.test.mjs && node test/classify.test.mjs"
87
92
  }
88
93
  }