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 +37 -18
- package/lib/client.js +47 -0
- package/lib/index.js +39 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,38 +1,57 @@
|
|
|
1
1
|
# dsh-done-sound
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness Web GUI
|
|
4
|
-
## 鍔熻兘
|
|
3
|
+
DeepSeek Harness Web GUI 的「对话完成音效」插件:你在设置里选一段音频,每次 Agent 完整结束一轮对话时自动播放提示音。
|
|
5
4
|
|
|
6
|
-
|
|
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
|
|
26
|
+
dsh plugin --profile web add link:F:\0.AI-CodeProject\DSHProject\0.日常聊天\dsh-done-sound
|
|
17
27
|
```
|
|
18
28
|
|
|
19
|
-
|
|
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 #
|
|
26
|
-
pnpm run build #
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
鍐呯疆鎻愮ず闊充笉澶熺敤锛熺ぞ鍖虹簿閫夐煶鑹插簱锛堢櫨搴︾綉鐩橈紝姘镐箙鏈夋晥锛夛細
|
|
50
|
+
- 链接:https://pan.baidu.com/s/1xnef4xkCy8pkooXMGi2ApQ?pwd=1234
|
|
51
|
+
- 提取码:1234
|
|
31
52
|
|
|
32
|
-
|
|
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
|
}
|