dsh-voice 0.3.2 → 0.3.4

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.en.md CHANGED
@@ -11,10 +11,12 @@ DSH (DeepSeek Harness) voice plugin pair: let the agent **speak and listen**.
11
11
  - **voice_tts**: text-to-speech over the **edge-tts protocol** (Microsoft Edge read-aloud service — free, unlimited, 22+ curated voices)
12
12
  - **voice_stt**: speech-to-text over any **OpenAI-compatible ASR endpoint** (Groq / OpenAI / custom)
13
13
  - **voice_list**: voice catalog
14
+ - **voice_preview**: batch-generate short preview MP3s to pick a voice by ear
15
+ - **voice_health**: offline self-check of TTS voice / ASR key / endpoint / proxy
14
16
 
15
17
  ## Compatibility
16
18
 
17
- Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` on 2026-08-31. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
19
+ Verified with official `@deepseek-ai/dsh@0.1.5-rc.1` and Node `24.16.0` on 2026-09-11: all 18 components load alongside Modlens, with passing tool-schema, skill-registration and offline read-only invocation checks. Uses the `cordis.patch.yml` + `dsh.bundle.patch` bundle model. Node requirements match this Harness release: 22.19 or later within 22.x, or 24 or later. Live external-service workflows require separate configuration and validation.
18
20
 
19
21
  ## Installation
20
22
 
@@ -53,6 +55,8 @@ Then restart the web service. To clean up fully, also remove the plugin entry fr
53
55
  | `voice_tts` | Synthesize MP3 from text (free) | `text` required; `voice`/`rate`/`pitch`/`output` optional |
54
56
  | `voice_stt` | Transcribe audio to text | `audio` required; `engine`/`model`/`language`/`prompt`/`output` optional |
55
57
  | `voice_list` | Curated voice catalog | none |
58
+ | `voice_preview` | Batch-generate short preview MP3s | optional `voices` (≤8) / `text` / `outputDir` |
59
+ | `voice_health` | Offline config self-check | none |
56
60
 
57
61
  ### Examples
58
62
 
@@ -61,6 +65,8 @@ voice_tts { text: hello world } # outputs voice_out
61
65
  voice_tts { text: hello, voice: en-US-AriaNeural } # English female voice
62
66
  voice_stt { audio: E:\audio\meeting.mp3, language: zh } # transcribe a recording
63
67
  voice_list {}
68
+ voice_preview { voices: [zh-CN-XiaoxiaoNeural, en-US-AriaNeural] } # generate two preview samples
69
+ voice_health {} # self-check TTS / ASR / proxy config
64
70
  ```
65
71
 
66
72
  ## Under the hood
@@ -81,3 +87,7 @@ pnpm test:integration # opt-in real edge-tts request; network and assertion err
81
87
  ## License
82
88
 
83
89
  MIT
90
+
91
+ ## Changelog
92
+
93
+ - **0.3.4 (2026-09-18)**: 修复代理路径 STT 上传 `[object FormData]`、25MB 校验在读入之后、输出目录不存在白烧一次合成、默认输出落宿主 cwd; `exec.signal` 全程透传、preview 改 3 路并发. 测试 55 项.
package/README.md CHANGED
@@ -14,10 +14,12 @@ DSH(DeepSeek Harness)语音双件套插件:让 agent **会说话、能听
14
14
  - **voice_tts**:文字转语音,走 **edge-tts 协议**(微软 Edge 朗读服务,免费无限量,22+ 常用音色)
15
15
  - **voice_stt**:语音转文字,走 **OpenAI 兼容 ASR 接口**(Groq / OpenAI / 自定义端点)
16
16
  - **voice_list**:音色清单
17
+ - **voice_preview**:音色试听,用一段样例文本批量生成短 MP3,挑音色不用盲选
18
+ - **voice_health**:配置自检,体检 TTS 音色 / ASR 密钥 / 接口地址 / 代理(不联网)
17
19
 
18
20
  ## 兼容性
19
21
 
20
- `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
22
+ 已在官方 `@deepseek-ai/dsh@0.1.5-rc.1`、Node `24.16.0` 上验证(2026-09-11):18 个组件与 Modlens 同载,工具 schema、技能注册及离线只读调用检查通过。采用 `cordis.patch.yml` + `dsh.bundle.patch` 组合包模型。Node 要求与该版本 Harness 一致:22.19 及以上的 22.x,或 24 及以上。外部服务的实际业务操作需按各组件配置单独验证。
21
23
 
22
24
  ## 安装
23
25
 
@@ -56,6 +58,8 @@ dsh plugin --profile web remove dsh-voice
56
58
  | `voice_tts` | 文字合成 MP3(免费) | `text` 必填;`voice`/`rate`/`pitch`/`output` 可选 |
57
59
  | `voice_stt` | 音频转文字 | `audio` 必填;`engine`/`model`/`language`/`prompt`/`output` 可选 |
58
60
  | `voice_list` | 常用音色清单 | 无 |
61
+ | `voice_preview` | 音色试听:批量生成短样例 MP3 | `voices`(≤8 个)/ `text` / `outputDir` 可选 |
62
+ | `voice_health` | 配置自检(不联网) | 无 |
59
63
 
60
64
  ### 示例
61
65
 
@@ -64,6 +68,8 @@ voice_tts { text: 今天的 AI 早报来了 } # 晓晓女声
64
68
  voice_tts { text: hello, voice: en-US-AriaNeural } # 英文女声
65
69
  voice_stt { audio: E:\audio\meeting.mp3, language: zh } # 转写会议录音
66
70
  voice_list {}
71
+ voice_preview { voices: [zh-CN-XiaoxiaoNeural, en-US-AriaNeural] } # 生成两个试听样例
72
+ voice_health {} # 自检 TTS / ASR / 代理配置
67
73
  ```
68
74
 
69
75
  ## 硬核细节
@@ -84,3 +90,7 @@ pnpm test:integration # 显式联网,调用真实 edge-tts;网络或断言
84
90
  ## License
85
91
 
86
92
  MIT
93
+
94
+ ## 版本记录
95
+
96
+ - **0.3.4(2026-09-18)**:修复代理路径 STT 上传 `[object FormData]`、25MB 校验在读入之后、输出目录不存在白烧一次合成、默认输出落宿主 cwd;`exec.signal` 全程透传、preview 改 3 路并发。测试 55 项。
package/lib/config.d.ts CHANGED
@@ -32,6 +32,7 @@ export interface ResolvedVoiceConfig {
32
32
  /** ASR 密钥:配置优先,其次环境变量 DSH_VOICE_ASR_KEY。 */
33
33
  export declare function resolveAsrApiKey(config: VoiceConfig | undefined, env?: NodeJS.ProcessEnv): string;
34
34
  /**
35
- * 解析并校验配置。
35
+ * 解析并校验配置:非法字段逐项回退到默认值,其余字段照常生效。
36
+ * @param onWarning 非法字段的告警回调,默认打到 console.warn。
36
37
  */
37
- export declare function resolveConfig(config: VoiceConfig | undefined | null): ResolvedVoiceConfig;
38
+ export declare function resolveConfig(config: VoiceConfig | undefined | null, onWarning?: (message: string) => void): ResolvedVoiceConfig;
package/lib/config.js CHANGED
@@ -14,10 +14,15 @@ export function resolveAsrApiKey(config, env = process.env) {
14
14
  return cfg.asrApiKey.trim();
15
15
  return env.DSH_VOICE_ASR_KEY?.trim() ?? '';
16
16
  }
17
+ /** 默认告警出口:非法字段逐项回退时点名,避免一个字段拖垮整份配置。 */
18
+ function defaultWarn(message) {
19
+ console.warn('[dsh-voice] ' + message);
20
+ }
17
21
  /**
18
- * 解析并校验配置。
22
+ * 解析并校验配置:非法字段逐项回退到默认值,其余字段照常生效。
23
+ * @param onWarning 非法字段的告警回调,默认打到 console.warn。
19
24
  */
20
- export function resolveConfig(config) {
25
+ export function resolveConfig(config, onWarning = defaultWarn) {
21
26
  const cfg = config ?? {};
22
27
  const ttsVoice = typeof cfg.ttsVoice === 'string' && cfg.ttsVoice.trim() !== '' ? cfg.ttsVoice.trim() : 'zh-CN-XiaoxiaoNeural';
23
28
  const ttsRate = typeof cfg.ttsRate === 'string' && cfg.ttsRate.trim() !== '' ? cfg.ttsRate.trim() : '+0%';
@@ -27,14 +32,19 @@ export function resolveConfig(config) {
27
32
  const asrBaseUrl = (typeof cfg.asrBaseUrl === 'string' && cfg.asrBaseUrl.trim() !== '' ? cfg.asrBaseUrl.trim() : preset?.baseUrl ?? '').replace(/\/$/, '');
28
33
  const asrModel = typeof cfg.asrModel === 'string' && cfg.asrModel.trim() !== '' ? cfg.asrModel.trim() : preset?.model ?? '';
29
34
  const asrApiKey = resolveAsrApiKey(cfg);
30
- const proxyUrl = (typeof cfg.proxyUrl === 'string' && cfg.proxyUrl.trim() !== '' ? cfg.proxyUrl.trim() : '');
31
- if (proxyUrl !== '' && !/^https?:\/\//i.test(proxyUrl))
32
- throw new Error('proxyUrl 必须是 http(s):// 开头的地址,例如 http://127.0.0.1:7890');
35
+ let proxyUrl = typeof cfg.proxyUrl === 'string' && cfg.proxyUrl.trim() !== '' ? cfg.proxyUrl.trim() : '';
36
+ if (proxyUrl !== '' && !/^https?:\/\//i.test(proxyUrl)) {
37
+ onWarning('proxyUrl 配置不合法(必须以 http(s):// 开头,例如 http://127.0.0.1:7890),已忽略该项,其余配置继续生效:' + proxyUrl);
38
+ proxyUrl = '';
39
+ }
33
40
  let timeoutMs = 60000;
34
41
  if (cfg.timeoutMs !== undefined) {
35
- if (typeof cfg.timeoutMs !== 'number' || !Number.isFinite(cfg.timeoutMs) || cfg.timeoutMs <= 0)
36
- throw new Error('timeoutMs 必须是大于 0 的数字(毫秒)。');
37
- timeoutMs = Math.min(10 * 60 * 1000, Math.max(5000, Math.round(cfg.timeoutMs)));
42
+ if (typeof cfg.timeoutMs !== 'number' || !Number.isFinite(cfg.timeoutMs) || cfg.timeoutMs <= 0) {
43
+ onWarning('timeoutMs 配置不合法(必须是大于 0 的数字,毫秒),已回退默认值 60000。');
44
+ }
45
+ else {
46
+ timeoutMs = Math.min(10 * 60 * 1000, Math.max(5000, Math.round(cfg.timeoutMs)));
47
+ }
38
48
  }
39
49
  const overwrite = cfg.overwrite === true;
40
50
  return { ttsVoice, ttsRate, ttsPitch, asrEngine, asrBaseUrl, asrApiKey, asrModel, proxyUrl, timeoutMs, overwrite };
package/lib/edge-tts.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface TtsDeps {
10
10
  webSocketFactory?: WebSocketFactory;
11
11
  nowSeconds?: () => number;
12
12
  proxyUrl?: string;
13
+ /** 取消信号(可选);也可通过 synthesizeSpeech 第 4 个参数传入。 */
14
+ signal?: AbortSignal;
13
15
  }
14
16
  /** 最小的 WebSocket 面。 */
15
17
  export interface WebSocketLike {
@@ -33,4 +35,4 @@ export declare function buildSsml(options: TtsOptions): string;
33
35
  * 合成语音,返回 MP3 字节。
34
36
  * @throws 文本为空/超长 / 连接失败 / 超时 / 无音频数据时抛中文错误。
35
37
  */
36
- export declare function synthesizeSpeech(options: TtsOptions, deps?: TtsDeps, timeoutMs?: number): Promise<Buffer>;
38
+ export declare function synthesizeSpeech(options: TtsOptions, deps?: TtsDeps, timeoutMs?: number, signal?: AbortSignal): Promise<Buffer>;
package/lib/edge-tts.js CHANGED
@@ -62,7 +62,10 @@ function protocolHeader(path, extra) {
62
62
  * 合成语音,返回 MP3 字节。
63
63
  * @throws 文本为空/超长 / 连接失败 / 超时 / 无音频数据时抛中文错误。
64
64
  */
65
- export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000) {
65
+ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000, signal) {
66
+ const abortSignal = signal ?? deps.signal;
67
+ if (abortSignal?.aborted === true)
68
+ throw abortSignal.reason;
66
69
  if (options.text.trim() === '')
67
70
  throw new Error('要合成的文本为空。');
68
71
  if (options.text.length > 5000)
@@ -77,17 +80,56 @@ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000) {
77
80
  const chunks = [];
78
81
  let done = false;
79
82
  await new Promise((resolvePromise, reject) => {
80
- const timer = setTimeout(() => {
83
+ const canListen = typeof abortSignal?.addEventListener === 'function';
84
+ let settled = false;
85
+ let timer;
86
+ let onAbort = () => { };
87
+ // 先结算再关 socket:即使 socket.close() 同步触发 close 事件,取消原因也不会被通用错误覆盖。
88
+ const fail = (error) => {
89
+ if (settled)
90
+ return;
91
+ settled = true;
92
+ if (timer !== undefined)
93
+ clearTimeout(timer);
94
+ if (canListen)
95
+ abortSignal.removeEventListener('abort', onAbort);
96
+ reject(error);
97
+ };
98
+ const finish = () => {
99
+ if (settled)
100
+ return;
101
+ settled = true;
102
+ if (timer !== undefined)
103
+ clearTimeout(timer);
104
+ if (canListen)
105
+ abortSignal.removeEventListener('abort', onAbort);
106
+ try {
107
+ socket.close();
108
+ }
109
+ catch { /* 忽略 */ }
110
+ resolvePromise();
111
+ };
112
+ onAbort = () => {
113
+ const reason = abortSignal?.reason ?? new Error('语音合成已取消。');
114
+ fail(reason);
115
+ try {
116
+ socket.close();
117
+ }
118
+ catch { /* 忽略 */ }
119
+ };
120
+ timer = setTimeout(() => {
121
+ fail(new Error('语音合成超时(' + timeoutMs + ' 毫秒无完整音频),请重试或检查网络。'));
81
122
  try {
82
123
  socket.close();
83
124
  }
84
125
  catch { /* 忽略 */ }
85
- reject(new Error('语音合成超时(' + timeoutMs + ' 毫秒无完整音频),请重试或检查网络。'));
86
126
  }, timeoutMs);
87
- const finish = () => { clearTimeout(timer); try {
88
- socket.close();
127
+ if (canListen)
128
+ abortSignal.addEventListener('abort', onAbort, { once: true });
129
+ if (abortSignal?.aborted === true) {
130
+ onAbort();
131
+ return;
89
132
  }
90
- catch { /* 忽略 */ } resolvePromise(); };
91
133
  socket.addEventListener('open', () => {
92
134
  const config = protocolHeader('speech.config', { 'Content-Type': 'application/json; charset=utf-8' })
93
135
  + JSON.stringify({
@@ -103,8 +145,7 @@ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000) {
103
145
  socket.send(ssml);
104
146
  }
105
147
  catch (error) {
106
- clearTimeout(timer);
107
- reject(error instanceof Error ? error : new Error(String(error)));
148
+ fail(error instanceof Error ? error : new Error(String(error)));
108
149
  }
109
150
  });
110
151
  socket.addEventListener('message', (event) => {
@@ -112,8 +153,7 @@ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000) {
112
153
  if (typeof data === 'string') {
113
154
  if (data.includes('Path:turn.end')) {
114
155
  if (chunks.length === 0) {
115
- clearTimeout(timer);
116
- reject(new Error('合成结束但没有收到音频数据(服务端可能拒绝了请求)。'));
156
+ fail(new Error('合成结束但没有收到音频数据(服务端可能拒绝了请求)。'));
117
157
  }
118
158
  else if (!done) {
119
159
  done = true;
@@ -132,18 +172,16 @@ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000) {
132
172
  }
133
173
  });
134
174
  socket.addEventListener('error', () => {
135
- clearTimeout(timer);
136
- reject(new Error('edge-tts WebSocket 连接失败。若网络需要特殊代理(梯子),请在 cordis.patch.yml 配置 proxyUrl 后重启。'));
175
+ fail(new Error('edge-tts WebSocket 连接失败。若网络需要特殊代理(梯子),请在 cordis.patch.yml 配置 proxyUrl 后重启。'));
137
176
  });
138
177
  socket.addEventListener('close', () => {
139
178
  if (!done) {
140
- clearTimeout(timer);
141
179
  if (chunks.length > 0) {
142
180
  done = true;
143
181
  finish();
144
182
  }
145
183
  else {
146
- reject(new Error('edge-tts 连接在收到音频前关闭。'));
184
+ fail(new Error('edge-tts 连接在收到音频前关闭。'));
147
185
  }
148
186
  }
149
187
  });
package/lib/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * dsh-voice —— 语音双件套工具插件(node 半身,配置走 cordis.patch.yml)。
3
3
  *
4
- * 插件导出 apply(ctx, config):注册三个面向模型的工具(voice_tts / voice_stt /
5
- * voice_list)。TTS 走 edge-tts 协议(原生 WebSocket,零 API 成本);STT 走 OpenAI
6
- * 兼容 ASR 接口(Groq/OpenAI/自定义端点)。
4
+ * 插件导出 apply(ctx, config):注册五个面向模型的工具(voice_tts / voice_stt /
5
+ * voice_list / voice_preview / voice_health)。TTS 走 edge-tts 协议(原生 WebSocket
6
+ * 零 API 成本);STT 走 OpenAI 兼容 ASR 接口(Groq/OpenAI/自定义端点)。
7
7
  *
8
8
  * @module dsh-voice
9
9
  */
@@ -20,7 +20,7 @@ export interface VoicePluginContext {
20
20
  on?(event: string, listener: () => void): () => void;
21
21
  }
22
22
  /**
23
- * 插件入口:解析配置并注册三个语音工具。
23
+ * 插件入口:解析配置并注册五个语音工具。
24
24
  */
25
25
  export declare function apply(ctx: VoicePluginContext, config?: VoiceConfig | null): void;
26
26
  export * from './config.js';
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * dsh-voice —— 语音双件套工具插件(node 半身,配置走 cordis.patch.yml)。
3
3
  *
4
- * 插件导出 apply(ctx, config):注册三个面向模型的工具(voice_tts / voice_stt /
5
- * voice_list)。TTS 走 edge-tts 协议(原生 WebSocket,零 API 成本);STT 走 OpenAI
6
- * 兼容 ASR 接口(Groq/OpenAI/自定义端点)。
4
+ * 插件导出 apply(ctx, config):注册五个面向模型的工具(voice_tts / voice_stt /
5
+ * voice_list / voice_preview / voice_health)。TTS 走 edge-tts 协议(原生 WebSocket
6
+ * 零 API 成本);STT 走 OpenAI 兼容 ASR 接口(Groq/OpenAI/自定义端点)。
7
7
  *
8
8
  * @module dsh-voice
9
9
  */
@@ -13,7 +13,7 @@ import { buildVoiceTools } from './tools.js';
13
13
  export const name = 'voice';
14
14
  export const inject = ['tools'];
15
15
  /**
16
- * 插件入口:解析配置并注册三个语音工具。
16
+ * 插件入口:解析配置并注册五个语音工具。
17
17
  */
18
18
  export function apply(ctx, config) {
19
19
  let cfg;
package/lib/paths.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** 校验音频文件存在且是文件;返回绝对路径。 */
2
- export declare function assertAudioFile(input: string): string;
3
- /** 决定输出路径:缺省放在当前工作目录,同名自动加 _1/_2 序号。 */
4
- export declare function resolveOutputPath(explicit: string | undefined, defaultName: string, overwrite: boolean): string;
1
+ /** 校验音频文件存在且是文件;相对路径按 cwd(会话工作区)解析,返回绝对路径。 */
2
+ export declare function assertAudioFile(input: string, cwd?: string): string;
3
+ /** 决定输出路径:缺省放在会话工作目录(缺省回退宿主 cwd),同名自动加 _1/_2 序号。 */
4
+ export declare function resolveOutputPath(explicit: string | undefined, defaultName: string, overwrite: boolean, cwd?: string): string;
package/lib/paths.js CHANGED
@@ -5,17 +5,17 @@
5
5
  */
6
6
  import { existsSync, statSync } from 'node:fs';
7
7
  import { basename, dirname, extname, join, resolve } from 'node:path';
8
- /** 校验音频文件存在且是文件;返回绝对路径。 */
9
- export function assertAudioFile(input) {
10
- const absolute = resolve(input);
8
+ /** 校验音频文件存在且是文件;相对路径按 cwd(会话工作区)解析,返回绝对路径。 */
9
+ export function assertAudioFile(input, cwd = process.cwd()) {
10
+ const absolute = resolve(cwd, input);
11
11
  if (!existsSync(absolute) || !statSync(absolute).isFile()) {
12
12
  throw new Error('音频文件不存在:' + input);
13
13
  }
14
14
  return absolute;
15
15
  }
16
- /** 决定输出路径:缺省放在当前工作目录,同名自动加 _1/_2 序号。 */
17
- export function resolveOutputPath(explicit, defaultName, overwrite) {
18
- let target = explicit !== undefined && explicit.trim() !== '' ? resolve(explicit.trim()) : resolve(process.cwd(), defaultName);
16
+ /** 决定输出路径:缺省放在会话工作目录(缺省回退宿主 cwd),同名自动加 _1/_2 序号。 */
17
+ export function resolveOutputPath(explicit, defaultName, overwrite, cwd = process.cwd()) {
18
+ let target = explicit !== undefined && explicit.trim() !== '' ? resolve(cwd, explicit.trim()) : resolve(cwd, defaultName);
19
19
  if (overwrite || !existsSync(target))
20
20
  return target;
21
21
  const directory = dirname(target);
@@ -1,4 +1,11 @@
1
1
  /**
2
- * 构造一个把所有请求路由到 proxyUrl fetch。
2
+ * 代理 fetch 一并带出 undici 自己的 FormData 构造器。
3
+ *
4
+ * undici 的 fetch/Response 只认自己那份 FormData(brand symbol 属于该模块),而 Node 的
5
+ * 全局 FormData 是另一份实现:喂错会被当成普通对象序列化成 "[object FormData]"
6
+ * (content-type: text/plain),ASR 接口必回 400 —— 实测见 README 的代理场景。
3
7
  */
4
- export declare function createProxyFetch(proxyUrl: string): typeof globalThis.fetch;
8
+ export type ProxyFetch = typeof globalThis.fetch & {
9
+ FormData: typeof globalThis.FormData;
10
+ };
11
+ export declare function createProxyFetch(proxyUrl: string): ProxyFetch;
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * 插件级 HTTP 代理:给 TTS 令牌与 ASR 请求一个走指定代理的 fetch,不影响同进程其他插件。
3
3
  */
4
- import { ProxyAgent, fetch as undiciFetch } from 'undici';
5
- /**
6
- * 构造一个把所有请求路由到 proxyUrl 的 fetch。
7
- */
4
+ import { FormData as UndiciFormData, ProxyAgent, fetch as undiciFetch } from 'undici';
8
5
  export function createProxyFetch(proxyUrl) {
9
6
  const agent = new ProxyAgent(proxyUrl);
10
- return ((input, init) => undiciFetch(input, { ...init, dispatcher: agent }));
7
+ const impl = ((input, init) => undiciFetch(input, { ...init, dispatcher: agent }));
8
+ impl.FormData = UndiciFormData;
9
+ return impl;
11
10
  }
package/lib/stt.d.ts CHANGED
@@ -5,12 +5,20 @@ export interface SttOptions {
5
5
  model: string;
6
6
  language?: string;
7
7
  prompt?: string;
8
+ /** 取消信号(可选);也可通过 transcribe 第 6 个参数传入。 */
9
+ signal?: AbortSignal;
8
10
  }
11
+ /** 字节数超过 25MB 时抛错:调用方可在读文件前先按 statSync 大小快速拒绝,避免白读整文件。 */
12
+ export declare function assertAudioSize(bytes: number): void;
9
13
  /**
10
14
  * 调用 OpenAI 兼容 ASR 接口转写音频。
11
15
  * @throws 缺密钥 / 文件过大 / HTTP 错误 / 无文本时抛中文错误。
12
16
  */
13
- export declare function transcribe(baseUrl: string, apiKey: string, options: SttOptions, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<{
17
+ /** 传给 transcribe fetch:可以像代理那样一并带上自己的 FormData/Blob 构造器。 */
18
+ export type SttFetch = typeof fetch & {
19
+ FormData?: typeof FormData;
20
+ };
21
+ export declare function transcribe(baseUrl: string, apiKey: string, options: SttOptions, fetchImpl?: SttFetch, timeoutMs?: number, signal?: AbortSignal): Promise<{
14
22
  text: string;
15
23
  model: string;
16
24
  }>;
package/lib/stt.js CHANGED
@@ -5,20 +5,44 @@
5
5
  */
6
6
  import { extname } from 'node:path';
7
7
  const MAX_AUDIO_BYTES = 25 * 1024 * 1024;
8
- /**
9
- * 调用 OpenAI 兼容 ASR 接口转写音频。
10
- * @throws 缺密钥 / 文件过大 / HTTP 错误 / 无文本时抛中文错误。
11
- */
12
- export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThis.fetch, timeoutMs = 120000) {
8
+ /** 字节数超过 25MB 时抛错:调用方可在读文件前先按 statSync 大小快速拒绝,避免白读整文件。 */
9
+ export function assertAudioSize(bytes) {
10
+ if (bytes > MAX_AUDIO_BYTES)
11
+ throw new Error('音频超过 25MB 上限(ASR 接口限制),请先用 ffmpeg 压缩。');
12
+ }
13
+ /** 已取消则抛出取消原因;作为 await 前后的统一取消检查。 */
14
+ function throwIfAborted(signal) {
15
+ if (signal?.aborted === true)
16
+ throw signal.reason;
17
+ }
18
+ /** 让不认 signal 的 fetch 实现也能在外部取消时立即 reject。 */
19
+ function raceAbort(value, signal) {
20
+ const promise = Promise.resolve(value);
21
+ if (signal === undefined || typeof signal.addEventListener !== 'function')
22
+ return promise;
23
+ if (signal.aborted === true)
24
+ return Promise.reject(signal.reason);
25
+ return new Promise((resolvePromise, rejectPromise) => {
26
+ const onAbort = () => rejectPromise(signal.reason);
27
+ signal.addEventListener('abort', onAbort, { once: true });
28
+ promise.then((settled) => { signal.removeEventListener('abort', onAbort); resolvePromise(settled); }, (error) => { signal.removeEventListener('abort', onAbort); rejectPromise(error); });
29
+ });
30
+ }
31
+ export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThis.fetch, timeoutMs = 120000, signal) {
32
+ const abortSignal = signal ?? options.signal;
33
+ throwIfAborted(abortSignal);
13
34
  if (apiKey === '')
14
35
  throw new Error('未配置 ASR 密钥:请设置环境变量 DSH_VOICE_ASR_KEY,或在 cordis.patch.yml 的 asrApiKey 配置后重启。');
15
36
  if (baseUrl === '')
16
37
  throw new Error('未配置 ASR 接口地址(asrEngine=custom 时必须提供 asrBaseUrl)。');
17
38
  if (options.audio.length === 0)
18
39
  throw new Error('音频文件为空。');
19
- if (options.audio.length > MAX_AUDIO_BYTES)
20
- throw new Error('音频超过 25MB 上限(ASR 接口限制),请先用 ffmpeg 压缩。');
21
- const form = new FormData();
40
+ assertAudioSize(options.audio.length);
41
+ // 代理 fetch(undici)只认它自己那份 FormData:喂 Node 全局 FormData 会被当成普通
42
+ // 对象序列化成 "[object FormData]"(content-type: text/plain),所以优先用调用方 fetch
43
+ // 附带的构造器。文件部分继续用全局 Blob —— 实测 undici 的 FormData 收它并输出真 multipart。
44
+ const FormDataImpl = fetchImpl.FormData ?? FormData;
45
+ const form = new FormDataImpl();
22
46
  const mime = mimeOf(options.filename);
23
47
  form.append('file', new Blob([options.audio], { type: mime }), options.filename);
24
48
  form.append('model', options.model);
@@ -26,16 +50,19 @@ export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThi
26
50
  form.append('language', options.language);
27
51
  if (options.prompt !== undefined && options.prompt !== '')
28
52
  form.append('prompt', options.prompt);
53
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
54
+ const requestSignal = abortSignal === undefined ? timeoutSignal : AbortSignal.any([timeoutSignal, abortSignal]);
29
55
  let response;
30
56
  try {
31
- response = await fetchImpl(baseUrl + '/audio/transcriptions', {
57
+ response = await raceAbort(fetchImpl(baseUrl + '/audio/transcriptions', {
32
58
  method: 'POST',
33
59
  headers: { authorization: 'Bearer ' + apiKey },
34
60
  body: form,
35
- signal: AbortSignal.timeout(timeoutMs),
36
- });
61
+ signal: requestSignal,
62
+ }), abortSignal);
37
63
  }
38
64
  catch (error) {
65
+ throwIfAborted(abortSignal);
39
66
  throw new Error('ASR 请求失败:' + (error instanceof Error ? error.message : String(error)) + '。若接口需要特殊代理(梯子),请在 cordis.patch.yml 配置 proxyUrl 后重启。');
40
67
  }
41
68
  if (!response.ok) {
package/lib/tools.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * 五个面向模型的语音工具:voice_tts / voice_stt / voice_list / voice_preview。
2
+ * 五个面向模型的语音工具:voice_tts / voice_stt / voice_list / voice_preview / voice_health
3
3
  *
4
4
  * @module dsh-voice/tools
5
5
  */
6
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { basename, join, resolve } from 'node:path';
6
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
7
+ import { basename, dirname, join, resolve } from 'node:path';
8
8
  import { synthesizeSpeech } from './edge-tts.js';
9
9
  import { assertAudioFile, resolveOutputPath } from './paths.js';
10
10
  import { createProxyFetch } from './proxy-fetch.js';
11
- import { transcribe } from './stt.js';
11
+ import { assertAudioSize, transcribe } from './stt.js';
12
12
  import { isValidVoiceId, VOICES } from './voices.js';
13
13
  function compileParameters(spec) {
14
14
  const properties = {};
@@ -21,6 +21,10 @@ function compileParameters(spec) {
21
21
  node.type = prop.type;
22
22
  if (typeof prop?.description === 'string')
23
23
  node.description = prop.description;
24
+ if (prop?.enum !== undefined)
25
+ node.enum = prop.enum;
26
+ if (prop?.items !== undefined)
27
+ node.items = prop.items;
24
28
  properties[key] = node;
25
29
  }
26
30
  return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
@@ -38,6 +42,56 @@ function requiredString(args, key, label) {
38
42
  throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
39
43
  return value;
40
44
  }
45
+ /** 取本次调用的取消信号;取不到就当未提供。 */
46
+ function execSignal(exec) {
47
+ const signal = exec?.signal;
48
+ return signal !== null && typeof signal === 'object' && typeof signal.aborted === 'boolean' ? signal : undefined;
49
+ }
50
+ /** 已取消则抛出取消原因;作为 await 前后的统一取消检查。 */
51
+ function throwIfAborted(signal) {
52
+ if (signal?.aborted === true)
53
+ throw signal.reason;
54
+ }
55
+ /** 让不认 signal 的实现也能在取消时立即 reject;signal 缺 addEventListener 时只能原样等待。 */
56
+ function raceAbort(value, signal) {
57
+ const promise = Promise.resolve(value);
58
+ if (signal === undefined || typeof signal.addEventListener !== 'function')
59
+ return promise;
60
+ if (signal.aborted === true)
61
+ return Promise.reject(signal.reason);
62
+ return new Promise((resolvePromise, rejectPromise) => {
63
+ const onAbort = () => rejectPromise(signal.reason);
64
+ signal.addEventListener('abort', onAbort, { once: true });
65
+ promise.then((settled) => { signal.removeEventListener('abort', onAbort); resolvePromise(settled); }, (error) => { signal.removeEventListener('abort', onAbort); rejectPromise(error); });
66
+ });
67
+ }
68
+ /** 有界并发:同一时刻最多跑 limit 个任务;取消时停止派发并抛出取消原因。 */
69
+ async function mapLimit(items, limit, signal, worker) {
70
+ const results = new Array(items.length);
71
+ let next = 0;
72
+ const run = async () => {
73
+ for (;;) {
74
+ throwIfAborted(signal);
75
+ const index = next;
76
+ next += 1;
77
+ if (index >= items.length)
78
+ return;
79
+ results[index] = await worker(items[index]);
80
+ }
81
+ };
82
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => run()));
83
+ return results;
84
+ }
85
+ /** 取本次调用的会话工作区;harness 从不 chdir,取不到时才回退宿主进程 cwd。 */
86
+ function sessionCwd(exec) {
87
+ const cwd = exec?.agent?.session?.header?.cwd;
88
+ return typeof cwd === 'string' && cwd.trim() !== '' ? cwd : process.cwd();
89
+ }
90
+ /** 写文件前先建父目录,避免合成/转写完成后才因 ENOENT 白烧一次接口调用。 */
91
+ function writeFileWithDir(target, data) {
92
+ mkdirSync(dirname(target), { recursive: true });
93
+ writeFileSync(target, data);
94
+ }
41
95
  const voiceItemSchema = {
42
96
  type: 'object',
43
97
  properties: { id: { type: 'string' }, gender: { type: 'string' }, locale: { type: 'string' } },
@@ -83,6 +137,8 @@ const previewSchema = {
83
137
  };
84
138
  /** 默认试听文本(中英混合,便于感知发音差异)。 */
85
139
  export const DEFAULT_PREVIEW_TEXT = '你好,这是音色试听。Hello, this is a voice preview.';
140
+ /** voice_preview 同时合成的音色数上限:有界并发,避免 8 个音色串行把最坏等待拖成 8 倍超时。 */
141
+ const PREVIEW_CONCURRENCY = 3;
86
142
  /**
87
143
  * 构建三个工具定义。
88
144
  */
@@ -119,7 +175,7 @@ export function buildVoiceTools(config, deps = {}) {
119
175
  voice: { type: 'string', description: '音色 id(可选,默认配置的 ttsVoice)。' },
120
176
  rate: { type: 'string', description: '语速,如 +20% 或 -10%(可选)。' },
121
177
  pitch: { type: 'string', description: '音调,如 +2Hz 或 -1Hz(可选)。' },
122
- output: { type: 'string', description: '输出 MP3 路径(可选)。' },
178
+ output: { type: 'string', description: '输出 MP3 路径(可选,相对路径按会话工作目录解析)。' },
123
179
  }),
124
180
  output: {
125
181
  schema: ttsSchema,
@@ -128,7 +184,9 @@ export function buildVoiceTools(config, deps = {}) {
128
184
  return [{ type: 'text', text: '语音合成完成:' + rec.output + '(' + rec.bytes + ' 字节,音色 ' + rec.voice + ')' }];
129
185
  },
130
186
  },
131
- async execute(rawArgs) {
187
+ async execute(rawArgs, exec) {
188
+ const signal = execSignal(exec);
189
+ throwIfAborted(signal);
132
190
  const args = asRecord(rawArgs);
133
191
  const text = requiredString(args, 'text', '要合成的文本');
134
192
  if (text.length > 5000)
@@ -142,9 +200,10 @@ export function buildVoiceTools(config, deps = {}) {
142
200
  if (!/^[+-]?\d+(\.\d+)?(%|Hz|st)$/.test(value))
143
201
  throw new Error(label + ' 不合法:' + value + '。合法格式如 +10%、-2Hz、+1st。');
144
202
  }
145
- const output = resolveOutputPath(optionalString(args, 'output'), 'voice_output.mp3', cfg.overwrite);
146
- const audio = await (deps.tts ?? synthesizeSpeech)({ text, voice, rate, pitch }, { proxyUrl: cfg.proxyUrl }, timeout);
147
- writeFileSync(output, audio);
203
+ const output = resolveOutputPath(optionalString(args, 'output'), 'voice_output.mp3', cfg.overwrite, sessionCwd(exec));
204
+ const audio = await raceAbort((deps.tts ?? synthesizeSpeech)({ text, voice, rate, pitch }, { proxyUrl: cfg.proxyUrl }, timeout, signal), signal);
205
+ throwIfAborted(signal);
206
+ writeFileWithDir(output, audio);
148
207
  return { output, bytes: audio.length, voice, rate, pitch, textLength: text.length };
149
208
  },
150
209
  timeoutMs: timeout + 10000,
@@ -154,11 +213,11 @@ export function buildVoiceTools(config, deps = {}) {
154
213
  description: '语音转文字:调用 OpenAI 兼容 ASR 接口(默认 Groq whisper-large-v3-turbo;可切 openai/custom)。audio 为音频文件路径(mp3/wav/m4a/ogg/flac,≤25MB);language/prompt 可选;output 可把转写文本写成 .txt。密钥用环境变量 DSH_VOICE_ASR_KEY 或配置 asrApiKey。',
155
214
  parameters: compileParameters({
156
215
  audio: { type: 'string', required: true, description: '音频文件路径(必填)。' },
157
- engine: { type: 'string', description: '引擎:groq / openai / custom(可选,默认配置值)。' },
216
+ engine: { type: 'string', enum: ['groq', 'openai', 'custom'], description: '引擎:groq / openai / custom(可选,默认配置值)。' },
158
217
  model: { type: 'string', description: '模型名(可选,覆盖配置)。' },
159
218
  language: { type: 'string', description: '语言提示,如 zh(可选)。' },
160
219
  prompt: { type: 'string', description: '提示词(专有名词/术语纠偏,可选)。' },
161
- output: { type: 'string', description: '把转写文本写成 .txt 的路径(可选)。' },
220
+ output: { type: 'string', description: '把转写文本写成 .txt 的路径(可选,相对路径按会话工作目录解析)。' },
162
221
  }),
163
222
  output: {
164
223
  schema: sttSchema,
@@ -167,9 +226,12 @@ export function buildVoiceTools(config, deps = {}) {
167
226
  return [{ type: 'text', text: '转写完成(模型 ' + rec.model + '):' + rec.text }];
168
227
  },
169
228
  },
170
- async execute(rawArgs) {
229
+ async execute(rawArgs, exec) {
230
+ const signal = execSignal(exec);
231
+ throwIfAborted(signal);
171
232
  const args = asRecord(rawArgs);
172
- const audioPath = assertAudioFile(requiredString(args, 'audio', '音频文件'));
233
+ const audioPath = assertAudioFile(requiredString(args, 'audio', '音频文件'), sessionCwd(exec));
234
+ assertAudioSize(statSync(audioPath).size);
173
235
  const engine = optionalString(args, 'engine') ?? cfg.asrEngine;
174
236
  let baseUrl;
175
237
  let model;
@@ -187,18 +249,19 @@ export function buildVoiceTools(config, deps = {}) {
187
249
  }
188
250
  else
189
251
  throw new Error('engine 必须是 groq / openai / custom 之一(当前:' + engine + ')。');
190
- const { text } = await (deps.stt ?? transcribe)(baseUrl, cfg.asrApiKey, {
252
+ const { text } = await raceAbort((deps.stt ?? transcribe)(baseUrl, cfg.asrApiKey, {
191
253
  audio: readFileSync(audioPath),
192
254
  filename: basename(audioPath),
193
255
  model,
194
256
  language: optionalString(args, 'language'),
195
257
  prompt: optionalString(args, 'prompt'),
196
- }, fetchImpl ?? globalThis.fetch, timeout);
258
+ }, fetchImpl ?? globalThis.fetch, timeout, signal), signal);
259
+ throwIfAborted(signal);
197
260
  let transcriptFile = '';
198
261
  const output = optionalString(args, 'output');
199
262
  if (output !== undefined) {
200
- const target = resolveOutputPath(output, '', cfg.overwrite);
201
- writeFileSync(target, text, 'utf8');
263
+ const target = resolveOutputPath(output, '', cfg.overwrite, sessionCwd(exec));
264
+ writeFileWithDir(target, text);
202
265
  transcriptFile = target;
203
266
  }
204
267
  // The output schema declares these as strings — never return null, or
@@ -209,11 +272,11 @@ export function buildVoiceTools(config, deps = {}) {
209
272
  };
210
273
  const voicePreview = {
211
274
  name: 'voice_preview',
212
- description: '音色试听:用一段固定试听文本批量生成短样例 MP3,方便挑选音色。voices 为音色 id 数组(可选,缺省 voice_list 前 4 个,最多 8 个);text 可自定义(≤200 字符);样例文件写入输出目录(默认工作目录 voice_previews),文件名含音色 id。单个音色失败不阻断其他。',
275
+ description: '音色试听:用一段固定试听文本批量生成短样例 MP3,方便挑选音色。voices 为音色 id 数组(可选,缺省 voice_list 前 4 个,最多 8 个);text 可自定义(≤200 字符);样例文件写入输出目录(默认会话工作目录 voice_previews),文件名含音色 id。单个音色失败不阻断其他。',
213
276
  parameters: compileParameters({
214
277
  voices: { type: 'array', items: { type: 'string' }, description: '音色 id 数组(可选,缺省 voice_list 前 4 个,最多 8 个)。' },
215
278
  text: { type: 'string', description: '试听文本(可选,默认中英混合试听句,≤200 字符)。' },
216
- outputDir: { type: 'string', description: '输出目录(可选,默认工作目录下 voice_previews)。' },
279
+ outputDir: { type: 'string', description: '输出目录(可选,默认会话工作目录下 voice_previews)。' },
217
280
  }),
218
281
  output: {
219
282
  schema: previewSchema,
@@ -233,7 +296,9 @@ export function buildVoiceTools(config, deps = {}) {
233
296
  return [{ type: 'text', text: lines.join('\n') }];
234
297
  },
235
298
  },
236
- async execute(rawArgs) {
299
+ async execute(rawArgs, exec) {
300
+ const signal = execSignal(exec);
301
+ throwIfAborted(signal);
237
302
  const args = asRecord(rawArgs);
238
303
  const rawVoices = Array.isArray(args.voices)
239
304
  ? args.voices.filter((v) => typeof v === 'string' && v.trim() !== '').map((v) => v.trim())
@@ -244,24 +309,32 @@ export function buildVoiceTools(config, deps = {}) {
244
309
  const text = optionalString(args, 'text') ?? DEFAULT_PREVIEW_TEXT;
245
310
  if (text.length > 200)
246
311
  throw new Error('试听文本请控制在 200 字以内(试听要短平快)。');
247
- const outDir = resolve(optionalString(args, 'outputDir') ?? 'voice_previews');
312
+ const outDir = resolve(sessionCwd(exec), optionalString(args, 'outputDir') ?? 'voice_previews');
248
313
  mkdirSync(outDir, { recursive: true });
249
- const samples = [];
250
- const failed = [];
251
- for (const voice of targets) {
314
+ const outcomes = await mapLimit(targets, PREVIEW_CONCURRENCY, signal, async (voice) => {
315
+ throwIfAborted(signal);
252
316
  if (!isValidVoiceId(voice)) {
253
- failed.push({ voice, error: '音色 id 不合法(应为 zh-CN-XXXNeural 形式的 edge 音色)' });
254
- continue;
317
+ return { voice, error: '音色 id 不合法(应为 zh-CN-XXXNeural 形式的 edge 音色)' };
255
318
  }
256
319
  try {
257
- const audio = await (deps.tts ?? synthesizeSpeech)({ text, voice, rate: cfg.ttsRate, pitch: cfg.ttsPitch }, { proxyUrl: cfg.proxyUrl }, timeout);
320
+ const audio = await raceAbort((deps.tts ?? synthesizeSpeech)({ text, voice, rate: cfg.ttsRate, pitch: cfg.ttsPitch }, { proxyUrl: cfg.proxyUrl }, timeout, signal), signal);
321
+ throwIfAborted(signal);
258
322
  const file = join(outDir, 'voice-preview-' + voice.replace(/[^a-zA-Z0-9-]/g, '_') + '.mp3');
259
323
  writeFileSync(file, audio);
260
- samples.push({ voice, output: file, bytes: audio.length });
324
+ return { voice, output: file, bytes: audio.length };
261
325
  }
262
326
  catch (error) {
263
- failed.push({ voice, error: error instanceof Error ? error.message : String(error) });
327
+ throwIfAborted(signal);
328
+ return { voice, error: error instanceof Error ? error.message : String(error) };
264
329
  }
330
+ });
331
+ const samples = [];
332
+ const failed = [];
333
+ for (const outcome of outcomes) {
334
+ if (outcome.error !== undefined)
335
+ failed.push({ voice: outcome.voice, error: outcome.error });
336
+ else
337
+ samples.push({ voice: outcome.voice, output: outcome.output, bytes: outcome.bytes });
265
338
  }
266
339
  return { count: samples.length, samples, failed, text };
267
340
  },
@@ -269,7 +342,7 @@ export function buildVoiceTools(config, deps = {}) {
269
342
  };
270
343
  const voiceHealth = {
271
344
  name: 'voice_health',
272
- description: 'dsh-voice 自检:检查 TTS 音色合法性、代理配置与 ASR 引擎/密钥就绪状态(不发起网络请求)。遇到问题时先运行本工具定位。',
345
+ description: 'dsh-voice 自检:检查 TTS 音色合法性、代理配置与 ASR 引擎/接口地址/密钥就绪状态(不发起网络请求)。遇到问题时先运行本工具定位。',
273
346
  parameters: compileParameters({}),
274
347
  output: {
275
348
  schema: { type: 'object', additionalProperties: true },
@@ -293,6 +366,10 @@ export function buildVoiceTools(config, deps = {}) {
293
366
  ok = false;
294
367
  checks.push({ name: '特殊代理', ok: true, detail: cfg.proxyUrl !== '' ? '已配置 ' + cfg.proxyUrl : '未配置' });
295
368
  checks.push({ name: 'ASR 引擎', ok: true, detail: cfg.asrEngine });
369
+ const baseUrlOk = cfg.asrBaseUrl !== '';
370
+ checks.push({ name: 'ASR 接口地址', ok: baseUrlOk, detail: baseUrlOk ? cfg.asrBaseUrl : '未配置:asrEngine=custom 时必须提供 asrBaseUrl。' });
371
+ if (!baseUrlOk)
372
+ ok = false;
296
373
  const hasKey = cfg.asrApiKey !== '' || typeof process.env.DSH_VOICE_ASR_KEY === 'string' && process.env.DSH_VOICE_ASR_KEY !== '';
297
374
  checks.push({ name: 'ASR 密钥', ok: hasKey, detail: hasKey ? '已配置' : '未配置:voice_stt 需要 DSH_VOICE_ASR_KEY 环境变量或配置 asrApiKey' });
298
375
  if (!hasKey)
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "dsh-voice",
3
- "version": "0.3.2",
4
- "description": "DSH 语音双件套插件:voice_tts(edge-tts 协议零成本微软神经语音合成)/ voice_stt(OpenAI 兼容 ASR 语音转文字)/ voice_list(音色列表),原生 WebSocket + 插件级代理。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/index.d.ts",
11
- "default": "./lib/index.js"
12
- },
13
- "./cordis.patch.yml": "./cordis.patch.yml",
14
- "./package.json": "./package.json"
15
- },
16
- "files": [
17
- "lib",
18
- "cordis.patch.yml",
19
- "README.md",
20
- "README.en.md"
21
- ],
22
- "scripts": {
23
- "build": "tsc -p tsconfig.json",
24
- "typecheck": "tsc -p tsconfig.json --noEmit",
1
+ {
2
+ "name": "dsh-voice",
3
+ "version": "0.3.4",
4
+ "description": "DSH 语音插件:edge-tts 微软神经语音合成(免费、无需密钥)+ OpenAI 兼容语音转文字,支持插件级代理。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
25
  "test": "pnpm run build && node --test \"test/*.test.mjs\"",
26
26
  "test:integration": "pnpm run build && node --test \"integration/*.test.mjs\"",
27
- "prepublishOnly": "pnpm run build"
28
- },
29
- "dsh": {
30
- "bundle": {
31
- "patch": "./cordis.patch.yml"
32
- }
33
- },
34
- "keywords": [
35
- "dsh",
36
- "deepseek-harness",
37
- "plugin",
38
- "tts",
39
- "stt",
40
- "whisper",
41
- "voice",
42
- "dsh-plugin"
43
- ],
44
- "license": "MIT",
45
- "engines": {
46
- "node": ">=22.19.0"
47
- },
48
- "dependencies": {
49
- "https-proxy-agent": "^9.1.0",
50
- "undici": "^8.10.0",
51
- "ws": "^8.21.3"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^24.0.0",
55
- "@types/ws": "^8.18.1",
56
- "typescript": "^5.6.0"
57
- },
58
- "repository": {
59
- "type": "git",
60
- "url": "git+https://github.com/STARDUSTLC666/dsh-voice.git"
61
- },
62
- "bugs": {
63
- "url": "https://github.com/STARDUSTLC666/dsh-voice/issues"
64
- },
65
- "homepage": "https://github.com/STARDUSTLC666/dsh-voice#readme",
66
- "author": "stardustlc",
67
- "packageManager": "pnpm@11.7.0"
68
- }
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "keywords": [
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "plugin",
38
+ "tts",
39
+ "stt",
40
+ "whisper",
41
+ "voice",
42
+ "dsh-plugin"
43
+ ],
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": "^22.19.0 || >=24.0.0"
47
+ },
48
+ "dependencies": {
49
+ "https-proxy-agent": "^9.1.0",
50
+ "undici": "^8.10.0",
51
+ "ws": "^8.21.3"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^24.0.0",
55
+ "@types/ws": "^8.18.1",
56
+ "typescript": "^5.6.0"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/STARDUSTLC666/dsh-voice.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/STARDUSTLC666/dsh-voice/issues"
64
+ },
65
+ "homepage": "https://github.com/STARDUSTLC666/dsh-voice#readme",
66
+ "author": "stardustlc",
67
+ "packageManager": "pnpm@11.7.0"
68
+ }