dsh-done-sound 0.1.4 → 0.1.5

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 CHANGED
@@ -1,38 +1,57 @@
1
1
  # dsh-done-sound
2
2
 
3
- DeepSeek Harness Web GUI 鐨勩€屽璇濆畬鎴愰煶鏁堛€嶆彃浠讹細浣犲湪璁剧疆閲岄€変竴娈甸煶棰戯紝姣忔 Agent 瀹屾暣缁撴潫涓€杞璇濇椂鑷姩鎾斁鎻愮ず闊炽€?
4
- ## 鍔熻兘
3
+ DeepSeek Harness Web GUI 的「对话完成音效」插件:你在设置里选一段音频,每次 Agent 完整结束一轮对话时自动播放提示音。
5
4
 
6
- - **璁剧疆鍗$墖**锛堣缃?鈫?瀵硅瘽瀹屾垚闊虫晥锛夛細閫夋嫨闊抽鏂囦欢锛坢p3 / wav / ogg / webm / m4a / aac / flac锛屸墹2MB锛夈€佽瘯鍚€佹竻闄ゃ€侀煶閲忚皟鑺傘€?- **瀹屾垚妫€娴?*锛氫互瀹夸富 `turn/end` 浜嬩欢鐨?reason锛堟甯?涓柇/鍑洪敊/瓒呴暱锛変负鏉冨▉瑙﹀彂淇″彿锛屽揩鎱㈠璇濋兘涓嶆紡銆佷笉璇垽锛涙瘡杞彧鍝嶄竴娆°€?- **瑙﹀彂寮€鍏?*锛? - `涓柇鏃朵篃鍝峘锛堥粯璁?*鍏?*锛夛細鎵嬪姩鍋滄鐢熸垚鏃舵槸鍚︿篃鎾斁銆? - `鍑洪敊鏃朵篃鍝峘锛堥粯璁?*寮€**锛夛細瀵硅瘽浠ラ敊璇粨鏉燂紙turn-error / 瓒呴暱鎴柇锛夋椂鏄惁涔熸挱鏀俱€?- 闊抽鏂囦欢淇濆瓨鍦?profile 鐩綍 `.dsh-done-sound/`锛岀粡 Host 璺敱 `/dsh-done-sound/audio/<fileId>` 鎾斁锛岄厤缃寔涔呭寲鍦ㄨ缃腑銆?
7
- ## 瀹夎
5
+ ## 功能
6
+
7
+ - **设置卡片**(设置 → 对话完成音效):选择音频文件(mp3 / wav / ogg / webm / m4a / aac / flac,≤2MB)、试听、清除、音量调节。
8
+ - **完成检测**:以宿主 `turn/end` 事件的 reason(正常/中断/出错/超长)为权威触发信号,快慢对话都不漏、不误判;每轮只响一次。
9
+ - **触发开关**:
10
+ - `中断时也响`(默认**关**):手动停止生成时是否也播放。
11
+ - `出错时也响`(默认**开**):对话以错误结束(turn-error / 超长截断)时是否也播放。
12
+ - **确认等待提醒**:对话中出现等待确认 / 提问时播放提示音。
13
+ - **出错自动重连**:对话报错后自动发送「继续」重试(可配置等待秒数)。
14
+ - **版本检查**:设置卡片显示当前版本与更新状态。
15
+ - 音频文件保存在 profile 目录 `.dsh-done-sound/`,经 Host 路由 `/dsh-done-sound/audio/<fileId>` 播放,配置持久化在设置中。
16
+
17
+ ## 安装
8
18
 
9
19
  ```sh
10
20
  dsh plugin --profile web add dsh-done-sound
11
21
  ```
12
22
 
13
- 鏈湴寮€鍙戯細
23
+ 本地开发:
14
24
 
15
25
  ```sh
16
- dsh plugin --profile web add link:F:\0.AI-CodeProject\DSHProject\0.鏃ュ父鑱婂ぉ\dsh-done-sound
26
+ dsh plugin --profile web add link:F:\0.AI-CodeProject\DSHProject\0.日常聊天\dsh-done-sound
17
27
  ```
18
28
 
19
- 瀹夎鍚庨噸鍚?`dsh web`锛屾墦寮€璁剧疆椤靛嵆鍙湅鍒般€屽璇濆畬鎴愰煶鏁堛€嶅崱鐗囥€?
20
- ## 鏋舵瀯
29
+ 安装后重启 `dsh web`,打开设置页即可看到「对话完成音效」卡片。
30
+
31
+ ## 架构
32
+
33
+ - **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 的模块解析)。
34
+ - **Client 半区**(`lib/client.js`):`settings.section` 设置卡片,以 fetch 调用 HTTP API,不依赖会话/命令 RPC;`conversation.session.header.utilities` 完成检测器(`useSession` 订阅 `ConversationSnapshot`,由 `partial/running → 空闲` 转移触发,按最后节点 `seq` 去重)。
35
+
36
+ ## 开发
21
37
 
22
- - **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`锛屼骇鐗╄嚜鍖呭惈锛坙ink 瀹夎鏃舵棤闇€渚濊禆 profile 鐨勬ā鍧楄В鏋愶級銆?- **Client 鍗婂尯**锛坄lib/client.js`锛夛細`settings.section` 璁剧疆鍗$墖锛?*fetch 璋?HTTP API锛屼笉渚濊禆浼氳瘽/鍛戒护 RPC**锛? `conversation.session.header.utilities` 瀹屾垚妫€娴嬪櫒锛坄useSession` 璁㈤槄 `ConversationSnapshot`锛宍partial/running 鈫?绌洪棽` 杞Щ瑙﹀彂锛屾寜鏈€鍚庤妭鐐?seq 鍘婚噸锛夈€?
23
- ## 寮€鍙?
24
38
  ```sh
25
- pnpm install # 瀹夎 esbuild
26
- pnpm run build # 鎵撳寘 src/index.js -> lib/index.js锛堣嚜鍖呭惈锛?pnpm run check # 璇硶妫€鏌ヤ袱涓骇鐗?```
39
+ pnpm install # 安装 esbuild
40
+ pnpm run build # 打包 src/index.js -> lib/index.js(自带)
41
+ pnpm run check # 语法检查两个产物
42
+ ```
43
+
44
+ 修改 host 源码后重新 `pnpm run build`;client 半区直接改 `lib/client.js`(`__ModuleLoader__` 格式即运行时契约,无需打包)。
45
+
46
+ ## 音效库下载
47
+
48
+ 内置提示音不够用?社区精选音色库(百度网盘,永久有效):
27
49
 
28
- 淇敼 host 婧愮爜鍚庨噸鏂?`pnpm run build`锛沜lient 鍗婂尯鐩存帴鏀?`lib/client.js`锛坄__ModuleLoader__` 鏍煎紡鍗宠繍琛屾椂濂戠害锛屾棤闇€鎵撳寘锛夈€?
29
- ## 闊虫晥搴撲笅杞?
30
- 鍐呯疆鎻愮ず闊充笉澶熺敤锛熺ぞ鍖虹簿閫夐煶鑹插簱锛堢櫨搴︾綉鐩橈紝姘镐箙鏈夋晥锛夛細
50
+ - 链接:https://pan.baidu.com/s/1xnef4xkCy8pkooXMGi2ApQ?pwd=1234
51
+ - 提取码:1234
31
52
 
32
- - 閾炬帴锛歨ttps://pan.baidu.com/s/1xnef4xkCy8pkooXMGi2ApQ?pwd=1234
33
- - 鎻愬彇鐮侊細1234
53
+ 下载解压后,在 设置 → 对话完成音效 → 选择音频 中选用即可。
34
54
 
35
- 涓嬭浇瑙e帇鍚庯紝鍦?璁剧疆 鈫?瀵硅瘽瀹屾垚闊虫晥 鈫?閫夋嫨闊抽 涓€夌敤鍗冲彲銆?
36
55
  ## License
37
56
 
38
57
  MIT
package/lib/client.js CHANGED
@@ -44,6 +44,7 @@ window.__ModuleLoader__.load({
44
44
  '.dtc-retry-chip .dtc-retry-ico{font-size:17px;line-height:1}',
45
45
  '.dtc-retry-chip .dtc-retry-num{font-size:19px;font-weight:800;margin-left:2px}',
46
46
  '.dtc-cooldown-line{color:var(--dsw-alias-state-warn-primary,#f59e0b);font-weight:600}',
47
+ '.dtc-logpath{word-break:break-all;text-align:right;max-width:70%;line-height:1.4}',
47
48
  ].join('');
48
49
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(CSS_ID) + ']') === null) {
49
50
  const tag = document.createElement('style');
@@ -83,6 +84,7 @@ window.__ModuleLoader__.load({
83
84
  retryArmed: '等待 {s} 秒后自动发送「继续」',
84
85
  retrySent: '已自动发送「继续」',
85
86
  retryFailed: '自动发送「继续」失败',
87
+ logPath: '日志文件',
86
88
  version: '当前版本',
87
89
  updateAvailable: '有新版本',
88
90
  upToDate: '已是最新',
@@ -129,6 +131,7 @@ window.__ModuleLoader__.load({
129
131
  retryArmed: 'will auto-send "continue" in {s}s',
130
132
  retrySent: 'auto-sent "continue"',
131
133
  retryFailed: 'failed to auto-send "continue"',
134
+ logPath: 'Log file',
132
135
  version: 'Version',
133
136
  updateAvailable: 'Update available',
134
137
  upToDate: 'Up to date',
@@ -224,6 +227,36 @@ window.__ModuleLoader__.load({
224
227
  react.useEffect(() => subscribeConfig(setCfg), []);
225
228
  return cfg;
226
229
  }
230
+ // ---- browser -> host log reporting ----
231
+ // Mirrors console output into the host's daily log file
232
+ // (<pluginRoot>/logs/<YYYYMMDD>/dsh-done-sound.log) so issues survive a
233
+ // closed DevTools. Must never call console itself (avoids recursion) and
234
+ // must never break the plugin when the report fails.
235
+ function reportLog(level, args) {
236
+ try {
237
+ const parts = [];
238
+ for (let i = 0; i < args.length; i++) {
239
+ const a = args[i];
240
+ try {
241
+ if (typeof a === 'string') parts.push(a);
242
+ else if (a instanceof Error) parts.push(a.stack || 'Error: ' + (a.message || String(a)));
243
+ else parts.push(JSON.stringify(a));
244
+ } catch {
245
+ parts.push(String(a));
246
+ }
247
+ }
248
+ const message = parts.join(' ');
249
+ if (!message) return;
250
+ fetch('/dsh-done-sound/api/log', {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({ level: level, source: 'client', message: message }),
254
+ }).catch(() => {});
255
+ } catch {
256
+ // reporting must never break the plugin
257
+ }
258
+ }
259
+
227
260
  function playChime(cfg) {
228
261
  const url = cfg.url || cfg.defaultUrl;
229
262
  if (!cfg.enabled || !url) return;
@@ -235,6 +268,7 @@ window.__ModuleLoader__.load({
235
268
  p.catch((err) => {
236
269
  try {
237
270
  console.warn('[dsh-done-sound] audio play failed (browser blocked it?):', err && err.message ? err.message : err);
271
+ reportLog('warn', ['audio play failed (browser blocked it?):', err && err.message ? err.message : err]);
238
272
  } catch {
239
273
  // ignore
240
274
  }
@@ -243,6 +277,7 @@ window.__ModuleLoader__.load({
243
277
  } catch (err) {
244
278
  try {
245
279
  console.warn('[dsh-done-sound] audio play failed:', err && err.message ? err.message : err);
280
+ reportLog('warn', ['audio play failed:', err && err.message ? err.message : err]);
246
281
  } catch {
247
282
  // ignore playback failures (autoplay policy, missing audio device)
248
283
  }
@@ -283,6 +318,7 @@ window.__ModuleLoader__.load({
283
318
  const fail = (why) => {
284
319
  try {
285
320
  console.error('[dsh-done-sound] auto-retry could not send "继续":', why);
321
+ reportLog('error', ['auto-retry could not send "继续":', why]);
286
322
  } catch {
287
323
  // console may be gone during teardown
288
324
  }
@@ -307,6 +343,7 @@ window.__ModuleLoader__.load({
307
343
  if (result && result.ok) {
308
344
  try {
309
345
  console.log('[dsh-done-sound] auto-retry sent "继续" to session', sid);
346
+ reportLog('info', ['auto-retry sent "继续" to session', sid]);
310
347
  } catch {
311
348
  // ignore
312
349
  }
@@ -369,6 +406,7 @@ window.__ModuleLoader__.load({
369
406
  autoRetryOnError: payload.autoRetryOnError !== false,
370
407
  retryDelaySeconds: typeof payload.retryDelaySeconds === 'number' ? payload.retryDelaySeconds : 60,
371
408
  version: typeof payload.version === 'string' ? payload.version : null,
409
+ logPath: typeof payload.logPath === 'string' && payload.logPath ? payload.logPath : null,
372
410
  url: typeof payload.url === 'string' && payload.url ? payload.url : null,
373
411
  defaultUrl: typeof payload.defaultUrl === 'string' && payload.defaultUrl ? payload.defaultUrl : null,
374
412
  fileName: payload.audio && typeof payload.audio.fileName === 'string' ? payload.audio.fileName : '',
@@ -568,6 +606,15 @@ window.__ModuleLoader__.load({
568
606
  : null,
569
607
  ),
570
608
 
609
+ cfg.logPath
610
+ ? react.createElement(
611
+ 'div',
612
+ { className: 'dtc-row' },
613
+ react.createElement('span', { className: 'dtc-muted' }, L('logPath') + ':'),
614
+ react.createElement('span', { className: 'dtc-muted dtc-logpath' }, cfg.logPath),
615
+ )
616
+ : null,
617
+
571
618
  react.createElement(
572
619
  'div',
573
620
  { className: 'dtc-row' },
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.js
2
2
  import { fileURLToPath } from "node:url";
3
- import { readFileSync } from "node:fs";
3
+ import { readFileSync, mkdirSync, appendFileSync } from "node:fs";
4
4
  import { mkdir, readFile, writeFile, unlink } from "node:fs/promises";
5
5
  import { join, dirname } from "node:path";
6
6
  import { randomUUID } from "node:crypto";
@@ -808,6 +808,24 @@ function pluginVersion() {
808
808
  }
809
809
  return cachedPluginVersion;
810
810
  }
811
+ var LOGS_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "logs");
812
+ function datedLogPath(now = /* @__PURE__ */ new Date()) {
813
+ const y = now.getFullYear();
814
+ const m = String(now.getMonth() + 1).padStart(2, "0");
815
+ const d = String(now.getDate()).padStart(2, "0");
816
+ return join(LOGS_ROOT, `${y}${m}${d}`, "dsh-done-sound.log");
817
+ }
818
+ function writeLog(level, message) {
819
+ try {
820
+ const now = /* @__PURE__ */ new Date();
821
+ const stamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
822
+ const file = datedLogPath(now);
823
+ mkdirSync(dirname(file), { recursive: true });
824
+ appendFileSync(file, `[${stamp}] [${level}] ${message}
825
+ `, "utf8");
826
+ } catch {
827
+ }
828
+ }
811
829
  var MAX_BYTES = 10 * 1024 * 1024;
812
830
  var DEFAULT_AUDIO_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "turn-done.wav");
813
831
  var DEFAULT_AUDIO_URL = "/dsh-done-sound/audio/default";
@@ -870,7 +888,8 @@ function apply(ctx) {
870
888
  size: value.audio?.size ?? 0
871
889
  },
872
890
  url: fileId ? `${ROUTE_PREFIX}/audio/${fileId}` : null,
873
- defaultUrl: DEFAULT_AUDIO_URL
891
+ defaultUrl: DEFAULT_AUDIO_URL,
892
+ logPath: datedLogPath()
874
893
  };
875
894
  };
876
895
  const clearAudio = async () => {
@@ -883,6 +902,7 @@ function apply(ctx) {
883
902
  }
884
903
  }
885
904
  await scope.update({ audio: { fileId: "", fileName: "", mime: "", size: 0 } });
905
+ writeLog("INFO", "audio cleared");
886
906
  };
887
907
  const setAudio = async (dataUrl, fileName) => {
888
908
  const match = /^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/.exec(dataUrl);
@@ -906,6 +926,7 @@ function apply(ctx) {
906
926
  } catch {
907
927
  }
908
928
  }
929
+ writeLog("INFO", `audio stored: ${fileName || "chime"} (${buffer.length} bytes, ${mime})`);
909
930
  return fileId;
910
931
  };
911
932
  ctx.commands.register({
@@ -962,6 +983,7 @@ function apply(ctx) {
962
983
  return { kind: "error", text: `\u672A\u77E5\u64CD\u4F5C\uFF1A${action}` };
963
984
  }
964
985
  } catch (error) {
986
+ writeLog("ERROR", `command failed: ${error instanceof Error ? error.message : String(error)}`);
965
987
  return { kind: "error", text: error instanceof Error ? error.message : String(error) };
966
988
  }
967
989
  }
@@ -1068,7 +1090,22 @@ function apply(ctx) {
1068
1090
  sendJson(res, 200, statusPayload());
1069
1091
  return;
1070
1092
  }
1093
+ if (pathname === "/dsh-done-sound/api/log" && method === "POST") {
1094
+ const body = await readJsonBody(req);
1095
+ const message = typeof body.message === "string" ? body.message : "";
1096
+ if (message) {
1097
+ const level = typeof body.level === "string" && /^[A-Za-z]+$/.test(body.level) ? body.level.toUpperCase() : "INFO";
1098
+ writeLog(body.source === "client" ? `CLIENT-${level}` : level, message.slice(0, 4e3));
1099
+ }
1100
+ sendJson(res, 200, { ok: true });
1101
+ return;
1102
+ }
1103
+ if (pathname === "/dsh-done-sound/api/log" && method === "GET") {
1104
+ sendJson(res, 200, { ok: true, logPath: datedLogPath() });
1105
+ return;
1106
+ }
1071
1107
  } catch (error) {
1108
+ writeLog("ERROR", `api ${method} ${pathname} failed: ${error instanceof Error ? error.message : String(error)}`);
1072
1109
  sendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
1073
1110
  return;
1074
1111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-done-sound",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",