@mingxy/ocosay 1.0.2 → 1.0.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.
Files changed (62) hide show
  1. package/README.md +21 -0
  2. package/dist/config.d.ts +21 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +14 -3
  5. package/dist/config.js.map +1 -1
  6. package/dist/plugin.js +1907 -138
  7. package/dist/plugin.js.map +7 -1
  8. package/package.json +4 -2
  9. package/TECH_PLAN.md +0 -352
  10. package/__mocks__/@opencode-ai/plugin.ts +0 -32
  11. package/jest.config.js +0 -15
  12. package/src/config.ts +0 -149
  13. package/src/core/backends/afplay-backend.ts +0 -162
  14. package/src/core/backends/aplay-backend.ts +0 -160
  15. package/src/core/backends/base.ts +0 -117
  16. package/src/core/backends/index.ts +0 -128
  17. package/src/core/backends/naudiodon-backend.ts +0 -164
  18. package/src/core/backends/powershell-backend.ts +0 -173
  19. package/src/core/player.ts +0 -322
  20. package/src/core/speaker.ts +0 -283
  21. package/src/core/stream-player.ts +0 -326
  22. package/src/core/stream-reader.ts +0 -190
  23. package/src/core/streaming-synthesizer.ts +0 -123
  24. package/src/core/types.ts +0 -185
  25. package/src/index.ts +0 -236
  26. package/src/plugin.ts +0 -178
  27. package/src/providers/base.ts +0 -150
  28. package/src/providers/minimax.ts +0 -515
  29. package/src/tools/tts.ts +0 -277
  30. package/src/types/config.ts +0 -38
  31. package/src/types/naudiodon.d.ts +0 -19
  32. package/tests/__mocks__/@opencode-ai/plugin.ts +0 -32
  33. package/tests/backends.test.ts +0 -831
  34. package/tests/config.test.ts +0 -327
  35. package/tests/index.test.ts +0 -201
  36. package/tests/integration-test.d.ts +0 -6
  37. package/tests/integration-test.d.ts.map +0 -1
  38. package/tests/integration-test.js +0 -84
  39. package/tests/integration-test.js.map +0 -1
  40. package/tests/integration-test.ts +0 -93
  41. package/tests/p1-fixes.test.ts +0 -160
  42. package/tests/plugin.test.ts +0 -312
  43. package/tests/provider.test.d.ts +0 -2
  44. package/tests/provider.test.d.ts.map +0 -1
  45. package/tests/provider.test.js +0 -69
  46. package/tests/provider.test.js.map +0 -1
  47. package/tests/provider.test.ts +0 -87
  48. package/tests/speaker.test.d.ts +0 -2
  49. package/tests/speaker.test.d.ts.map +0 -1
  50. package/tests/speaker.test.js +0 -63
  51. package/tests/speaker.test.js.map +0 -1
  52. package/tests/speaker.test.ts +0 -232
  53. package/tests/stream-player.test.ts +0 -303
  54. package/tests/stream-reader.test.ts +0 -269
  55. package/tests/streaming-synthesizer.test.ts +0 -225
  56. package/tests/tts-tools.test.ts +0 -270
  57. package/tests/types.test.d.ts +0 -2
  58. package/tests/types.test.d.ts.map +0 -1
  59. package/tests/types.test.js +0 -61
  60. package/tests/types.test.js.map +0 -1
  61. package/tests/types.test.ts +0 -63
  62. package/tsconfig.json +0 -22
package/dist/plugin.js CHANGED
@@ -1,162 +1,1931 @@
1
- import { tool } from '@opencode-ai/plugin';
2
- import { handleToolCall } from './index';
3
- import { initialize } from './index';
4
- import { loadOrCreateConfig } from './config';
5
- const pluginName = 'ocosay';
6
- const ttsSpeakTool = tool({
7
- description: '将文本转换为语音并播放',
8
- args: {
9
- text: tool.schema.string().describe('要转换的文本内容'),
10
- provider: tool.schema.string().optional().describe('TTS 提供商名称'),
11
- voice: tool.schema.string().optional().describe('音色 ID'),
12
- model: tool.schema.enum(['sync', 'async', 'stream']).optional().describe('合成模式'),
13
- speed: tool.schema.number().optional().describe('语速 0.5-2.0'),
14
- volume: tool.schema.number().optional().describe('音量 0-100'),
15
- pitch: tool.schema.number().optional().describe('音调 0.5-2.0')
16
- },
17
- execute: async (args) => {
18
- const result = await handleToolCall('tts_speak', args);
19
- if (result.success === false) {
20
- throw new Error(result.error || 'Unknown error');
21
- }
22
- return result;
1
+ // src/plugin.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
4
+ // src/core/speaker.ts
5
+ import { EventEmitter as EventEmitter2 } from "events";
6
+
7
+ // src/core/types.ts
8
+ var TTSError = class extends Error {
9
+ constructor(message, code, provider, details) {
10
+ super(message);
11
+ this.name = "TTSError";
12
+ this.code = code;
13
+ this.provider = provider;
14
+ this.details = details;
15
+ }
16
+ code;
17
+ provider;
18
+ details;
19
+ };
20
+
21
+ // src/providers/base.ts
22
+ var providers = /* @__PURE__ */ new Map();
23
+ function registerProvider(name, provider) {
24
+ if (providers.has(name)) {
25
+ throw new TTSError(
26
+ `Provider "${name}" is already registered`,
27
+ "UNKNOWN" /* UNKNOWN */,
28
+ "system"
29
+ );
30
+ }
31
+ providers.set(name, provider);
32
+ }
33
+ function getProvider(name) {
34
+ const provider = providers.get(name);
35
+ if (!provider) {
36
+ throw new TTSError(
37
+ `TTS Provider "${name}" not found`,
38
+ "UNKNOWN" /* UNKNOWN */,
39
+ "system"
40
+ );
41
+ }
42
+ return provider;
43
+ }
44
+ function listProviders() {
45
+ return Array.from(providers.keys());
46
+ }
47
+ function hasProvider(name) {
48
+ return providers.has(name);
49
+ }
50
+ var BaseTTSProvider = class {
51
+ apiKey;
52
+ defaultVoice;
53
+ defaultModel = "stream";
54
+ async initialize() {
55
+ }
56
+ async destroy() {
57
+ }
58
+ /**
59
+ * 通用 speak 实现,处理通用逻辑
60
+ */
61
+ async speak(text, options) {
62
+ if (!text || text.trim().length === 0) {
63
+ throw new TTSError(
64
+ "Text cannot be empty",
65
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
66
+ this.name
67
+ );
23
68
  }
24
- });
25
- const ttsStopTool = tool({
26
- description: '停止当前 TTS 播放',
27
- args: {},
28
- execute: async () => {
29
- const result = await handleToolCall('tts_stop');
30
- if (result.success === false) {
31
- throw new Error(result.error || 'Unknown error');
32
- }
33
- return result;
69
+ const voice = options?.voice || this.defaultVoice;
70
+ const model = options?.model || this.defaultModel;
71
+ return this.doSpeak(text, voice, model, options);
72
+ }
73
+ pause() {
74
+ throw new TTSError(
75
+ "Pause is not supported by this provider",
76
+ "UNKNOWN" /* UNKNOWN */,
77
+ this.name
78
+ );
79
+ }
80
+ resume() {
81
+ throw new TTSError(
82
+ "Resume is not supported by this provider",
83
+ "UNKNOWN" /* UNKNOWN */,
84
+ this.name
85
+ );
86
+ }
87
+ stop() {
88
+ return Promise.resolve();
89
+ }
90
+ async listVoices() {
91
+ return [];
92
+ }
93
+ getCapabilities() {
94
+ return this.capabilities;
95
+ }
96
+ /**
97
+ * 验证 API Key
98
+ */
99
+ validateApiKey() {
100
+ if (!this.apiKey) {
101
+ throw new TTSError(
102
+ `API key is required for provider "${this.name}"`,
103
+ "AUTH" /* AUTH */,
104
+ this.name
105
+ );
34
106
  }
35
- });
36
- const ttsPauseTool = tool({
37
- description: '暂停当前 TTS 播放',
38
- args: {},
39
- execute: async () => {
40
- const result = await handleToolCall('tts_pause');
41
- if (result.success === false) {
42
- throw new Error(result.error || 'Unknown error');
107
+ }
108
+ };
109
+
110
+ // src/core/player.ts
111
+ import { EventEmitter } from "events";
112
+ import { spawn } from "child_process";
113
+ import fs from "fs";
114
+ import { createWriteStream } from "fs";
115
+ import { tmpdir } from "os";
116
+ import { join } from "path";
117
+ var AudioPlayer = class extends EventEmitter {
118
+ constructor(events) {
119
+ super();
120
+ this.events = events;
121
+ }
122
+ events;
123
+ _playing = false;
124
+ _paused = false;
125
+ currentProcess;
126
+ currentFile;
127
+ /**
128
+ * 播放音频
129
+ * @param audioData 音频数据 (Buffer 或 ReadableStream)
130
+ * @param format 音频格式 (mp3, wav, etc.)
131
+ */
132
+ async play(audioData, format) {
133
+ if (this._playing) {
134
+ await this.stop();
135
+ }
136
+ this._playing = true;
137
+ this._paused = false;
138
+ try {
139
+ const tempFile = join(tmpdir(), `ocosay-${Date.now()}.${format}`);
140
+ this.currentFile = tempFile;
141
+ if (Buffer.isBuffer(audioData)) {
142
+ fs.writeFileSync(tempFile, audioData);
143
+ } else {
144
+ const writeStream = createWriteStream(tempFile);
145
+ const reader = audioData.getReader();
146
+ try {
147
+ while (true) {
148
+ const { done, value } = await reader.read();
149
+ if (done) break;
150
+ if (value instanceof Uint8Array) {
151
+ writeStream.write(Buffer.from(value));
152
+ }
153
+ }
154
+ writeStream.end();
155
+ } finally {
156
+ reader.releaseLock();
43
157
  }
44
- return result;
158
+ await new Promise((resolve, reject) => {
159
+ writeStream.on("finish", resolve);
160
+ writeStream.on("error", reject);
161
+ });
162
+ }
163
+ this.events?.onStart?.();
164
+ this.emit("start");
165
+ await this.playFile(tempFile, format);
166
+ this._playing = false;
167
+ this.events?.onEnd?.();
168
+ this.emit("end");
169
+ this.cleanup();
170
+ } catch (error) {
171
+ this._playing = false;
172
+ const ttsError = new TTSError(
173
+ error.message || "Playback failed",
174
+ "PLAYER_ERROR" /* PLAYER_ERROR */,
175
+ "player"
176
+ );
177
+ this.events?.onError?.(ttsError);
178
+ this.emit("error", ttsError);
179
+ throw ttsError;
45
180
  }
46
- });
47
- const ttsResumeTool = tool({
48
- description: '恢复暂停的 TTS 播放',
49
- args: {},
50
- execute: async () => {
51
- const result = await handleToolCall('tts_resume');
52
- if (result.success === false) {
53
- throw new Error(result.error || 'Unknown error');
181
+ }
182
+ /**
183
+ * 播放音频文件
184
+ * 优先使用 afplay (macOS), aplay (Linux), 否则用 PowerShell (Windows)
185
+ */
186
+ playFile(filePath, format) {
187
+ const platform = process.platform;
188
+ return new Promise((resolve, reject) => {
189
+ let command;
190
+ let args;
191
+ if (platform === "darwin") {
192
+ command = "afplay";
193
+ args = [filePath];
194
+ } else if (platform === "linux") {
195
+ command = "aplay";
196
+ args = [filePath];
197
+ } else {
198
+ command = "powershell";
199
+ args = ["-c", `(New-Object System.Media.SoundPlayer('${filePath.replace(/\\/g, "\\\\")}')).PlaySync()`];
200
+ }
201
+ this.currentProcess = spawn(command, args, {
202
+ stdio: "ignore",
203
+ detached: false
204
+ });
205
+ this.currentProcess.on("exit", (code, signal) => {
206
+ if (signal === "SIGTERM" || signal === "SIGINT") {
207
+ resolve();
208
+ } else if (code === 0) {
209
+ resolve();
210
+ } else {
211
+ reject(new Error(`Player exited with code ${code}`));
54
212
  }
55
- return result;
213
+ });
214
+ this.currentProcess.on("error", (error) => {
215
+ reject(error);
216
+ });
217
+ });
218
+ }
219
+ /**
220
+ * 暂停播放
221
+ * 注意: 目前通过 SIGSTOP 实现,真正的 pause 需要支持暂停的音频库
222
+ */
223
+ pause() {
224
+ if (!this._playing || this._paused) return;
225
+ if (this.currentProcess) {
226
+ try {
227
+ this.currentProcess.kill("SIGSTOP");
228
+ this._paused = true;
229
+ this.events?.onPause?.();
230
+ this.emit("pause");
231
+ } catch (e) {
232
+ }
56
233
  }
57
- });
58
- const ttsListVoicesTool = tool({
59
- description: '列出可用的音色',
60
- args: {
61
- provider: tool.schema.string().optional().describe('TTS 提供商名称')
62
- },
63
- execute: async (args) => {
64
- const result = await handleToolCall('tts_list_voices', args);
65
- if (result.success === false) {
66
- throw new Error(result.error || 'Unknown error');
234
+ }
235
+ /**
236
+ * 恢复播放
237
+ */
238
+ resume() {
239
+ if (!this._playing || !this._paused) return;
240
+ if (this.currentProcess) {
241
+ try {
242
+ this.currentProcess.kill("SIGCONT");
243
+ this._paused = false;
244
+ this.events?.onResume?.();
245
+ this.emit("resume");
246
+ } catch (e) {
247
+ }
248
+ }
249
+ }
250
+ /**
251
+ * 停止播放
252
+ */
253
+ async stop() {
254
+ this._playing = false;
255
+ this._paused = false;
256
+ if (this.currentProcess) {
257
+ try {
258
+ this.currentProcess.kill("SIGTERM");
259
+ } catch (e) {
260
+ }
261
+ this.currentProcess = void 0;
262
+ }
263
+ this.cleanup();
264
+ this.events?.onStop?.();
265
+ this.emit("stop");
266
+ }
267
+ /**
268
+ * 是否正在播放
269
+ */
270
+ isPlaying() {
271
+ return this._playing;
272
+ }
273
+ /**
274
+ * 是否暂停
275
+ */
276
+ isPaused() {
277
+ return this._paused;
278
+ }
279
+ /**
280
+ * 清理临时文件
281
+ */
282
+ cleanup() {
283
+ if (this.currentFile) {
284
+ try {
285
+ if (fs.existsSync(this.currentFile)) {
286
+ fs.unlinkSync(this.currentFile);
67
287
  }
68
- return result;
288
+ } catch (e) {
289
+ }
290
+ this.currentFile = void 0;
69
291
  }
70
- });
71
- const ttsListProvidersTool = tool({
72
- description: '列出所有已注册的 TTS 提供商',
73
- args: {},
74
- execute: async () => {
75
- const result = await handleToolCall('tts_list_providers');
76
- if (result.success === false) {
77
- throw new Error(result.error || 'Unknown error');
292
+ }
293
+ };
294
+
295
+ // src/core/speaker.ts
296
+ var Speaker = class extends EventEmitter2 {
297
+ constructor(options = {}) {
298
+ super();
299
+ this.options = options;
300
+ const playerEvents = {
301
+ onStart: () => this.emit("start", this.currentText),
302
+ onEnd: () => {
303
+ this.isSpeaking = false;
304
+ this.emit("end", this.currentText);
305
+ },
306
+ onError: (error) => this.emit("error", error),
307
+ onPause: () => {
308
+ this.isPaused = true;
309
+ this.emit("pause");
310
+ },
311
+ onResume: () => {
312
+ this.isPaused = false;
313
+ this.emit("resume");
314
+ },
315
+ onStop: () => {
316
+ this.isSpeaking = false;
317
+ this.isPaused = false;
318
+ this.emit("stop");
319
+ }
320
+ };
321
+ this.player = new AudioPlayer(playerEvents);
322
+ }
323
+ options;
324
+ currentProvider;
325
+ player;
326
+ currentText;
327
+ isSpeaking = false;
328
+ isPaused = false;
329
+ /**
330
+ * 说话 - 核心方法
331
+ * @param text 要说的文本
332
+ * @param options 可选参数
333
+ */
334
+ async speak(text, options = {}) {
335
+ if (!text || text.trim().length === 0) {
336
+ throw new TTSError(
337
+ "Text cannot be empty",
338
+ "INVALID_PARAMS" /* INVALID_PARAMS */,
339
+ "speaker"
340
+ );
341
+ }
342
+ if (this.isSpeaking) {
343
+ await this.stop();
344
+ }
345
+ this.isSpeaking = true;
346
+ this.currentText = text;
347
+ try {
348
+ const providerName = options.provider || this.options.defaultProvider || "minimax";
349
+ if (!hasProvider(providerName)) {
350
+ throw new TTSError(
351
+ `Provider "${providerName}" not found`,
352
+ "UNKNOWN" /* UNKNOWN */,
353
+ "speaker"
354
+ );
355
+ }
356
+ this.currentProvider = getProvider(providerName);
357
+ const result = await this.currentProvider.speak(text, {
358
+ model: options.model || this.options.defaultModel || "stream",
359
+ voice: options.voice || this.options.defaultVoice,
360
+ speed: options.speed,
361
+ volume: options.volume,
362
+ pitch: options.pitch,
363
+ sourceVoice: options.sourceVoice
364
+ });
365
+ if (this.player) {
366
+ await this.player.play(result.audioData, result.format);
367
+ }
368
+ } catch (error) {
369
+ this.isSpeaking = false;
370
+ if (error instanceof TTSError) {
371
+ this.emit("error", error);
372
+ throw error;
373
+ }
374
+ const ttsError = new TTSError(
375
+ "Speak failed",
376
+ "UNKNOWN" /* UNKNOWN */,
377
+ "speaker",
378
+ error
379
+ );
380
+ this.emit("error", ttsError);
381
+ throw ttsError;
382
+ }
383
+ }
384
+ /**
385
+ * 暂停播放
386
+ */
387
+ pause() {
388
+ if (this.player && this.isSpeaking && !this.isPaused) {
389
+ this.player.pause();
390
+ }
391
+ }
392
+ /**
393
+ * 恢复播放
394
+ */
395
+ resume() {
396
+ if (this.player && this.isPaused) {
397
+ this.player.resume();
398
+ }
399
+ }
400
+ /**
401
+ * 停止播放
402
+ */
403
+ async stop() {
404
+ this.isSpeaking = false;
405
+ this.isPaused = false;
406
+ if (this.player) {
407
+ await this.player.stop();
408
+ }
409
+ }
410
+ /**
411
+ * 销毁 Speaker,释放资源
412
+ */
413
+ async destroy() {
414
+ this.isSpeaking = false;
415
+ this.isPaused = false;
416
+ if (this.player) {
417
+ await this.player.stop();
418
+ this.player = void 0;
419
+ }
420
+ this.currentProvider = void 0;
421
+ this.currentText = void 0;
422
+ }
423
+ /**
424
+ * 列出可用音色
425
+ */
426
+ async listVoices(providerName) {
427
+ const name = providerName || this.options.defaultProvider || "minimax";
428
+ const provider = getProvider(name);
429
+ return provider.listVoices();
430
+ }
431
+ /**
432
+ * 获取 Provider 能力
433
+ */
434
+ getCapabilities(providerName) {
435
+ const name = providerName || this.options.defaultProvider || "minimax";
436
+ const provider = getProvider(name);
437
+ return provider.getCapabilities();
438
+ }
439
+ /**
440
+ * 获取所有已注册的 Provider
441
+ */
442
+ getProviders() {
443
+ return listProviders();
444
+ }
445
+ /**
446
+ * 是否正在播放
447
+ */
448
+ isPlaying() {
449
+ return this.isSpeaking && !this.isPaused;
450
+ }
451
+ /**
452
+ * 是否暂停
453
+ */
454
+ isPausedState() {
455
+ return this.isPaused;
456
+ }
457
+ };
458
+ var defaultSpeaker;
459
+ function getDefaultSpeaker() {
460
+ if (!defaultSpeaker) {
461
+ defaultSpeaker = new Speaker();
462
+ }
463
+ return defaultSpeaker;
464
+ }
465
+ async function speak(text, options) {
466
+ const speaker2 = getDefaultSpeaker();
467
+ return speaker2.speak(text, options);
468
+ }
469
+ async function stop() {
470
+ const speaker2 = getDefaultSpeaker();
471
+ return speaker2.stop();
472
+ }
473
+ function pause() {
474
+ const speaker2 = getDefaultSpeaker();
475
+ speaker2.pause();
476
+ }
477
+ function resume() {
478
+ const speaker2 = getDefaultSpeaker();
479
+ speaker2.resume();
480
+ }
481
+ async function listVoices(providerName) {
482
+ const speaker2 = getDefaultSpeaker();
483
+ return speaker2.listVoices(providerName);
484
+ }
485
+
486
+ // src/tools/tts.ts
487
+ async function handleToolCall(toolName, args) {
488
+ try {
489
+ switch (toolName) {
490
+ case "tts_speak":
491
+ await speak(args?.text, {
492
+ provider: args?.provider,
493
+ voice: args?.voice,
494
+ model: args?.model,
495
+ speed: args?.speed,
496
+ volume: args?.volume,
497
+ pitch: args?.pitch
498
+ });
499
+ return { success: true, message: "Speech completed" };
500
+ case "tts_stop":
501
+ await stop();
502
+ return { success: true, message: "Stopped" };
503
+ case "tts_pause":
504
+ pause();
505
+ return { success: true, message: "Paused" };
506
+ case "tts_resume":
507
+ resume();
508
+ return { success: true, message: "Resumed" };
509
+ case "tts_list_voices":
510
+ const voices = await listVoices(args?.provider);
511
+ return { success: true, voices };
512
+ case "tts_list_providers":
513
+ const speaker2 = getDefaultSpeaker();
514
+ const providers2 = speaker2.getProviders();
515
+ return { success: true, providers: providers2 };
516
+ case "tts_status":
517
+ const s = getDefaultSpeaker();
518
+ return {
519
+ success: true,
520
+ isPlaying: s.isPlaying(),
521
+ isPaused: s.isPausedState()
522
+ };
523
+ case "tts_stream_speak":
524
+ if (!isAutoReadEnabled()) {
525
+ throw new TTSError(
526
+ "Stream mode is not enabled. autoRead must be enabled in configuration to use tts_stream_speak.",
527
+ "UNKNOWN" /* UNKNOWN */,
528
+ "tts_stream"
529
+ );
530
+ }
531
+ if (!isStreamEnabled()) {
532
+ throw new TTSError(
533
+ "Stream components not initialized. Please initialize with autoRead enabled.",
534
+ "UNKNOWN" /* UNKNOWN */,
535
+ "tts_stream"
536
+ );
78
537
  }
79
- return result;
538
+ const streamReader2 = getStreamReader();
539
+ const synthesizer = getStreamingSynthesizer();
540
+ if (streamReader2 && synthesizer) {
541
+ streamReader2.start();
542
+ if (args?.text) {
543
+ synthesizer.synthesize(args.text);
544
+ }
545
+ return { success: true, message: "Stream speak started" };
546
+ }
547
+ throw new TTSError(
548
+ "Stream components not available",
549
+ "UNKNOWN" /* UNKNOWN */,
550
+ "tts_stream"
551
+ );
552
+ case "tts_stream_stop":
553
+ if (!isStreamEnabled()) {
554
+ throw new TTSError(
555
+ "Stream mode is not enabled. Please enable autoRead in configuration.",
556
+ "UNKNOWN" /* UNKNOWN */,
557
+ "tts_stream"
558
+ );
559
+ }
560
+ const player = getStreamPlayer();
561
+ if (player) {
562
+ player.stop();
563
+ return { success: true, message: "Stream stopped" };
564
+ }
565
+ throw new TTSError(
566
+ "Stream player not available",
567
+ "UNKNOWN" /* UNKNOWN */,
568
+ "tts_stream"
569
+ );
570
+ case "tts_stream_status":
571
+ if (!isStreamEnabled()) {
572
+ return {
573
+ success: true,
574
+ isActive: false,
575
+ bytesWritten: 0,
576
+ state: "not_initialized"
577
+ };
578
+ }
579
+ return {
580
+ success: true,
581
+ ...getStreamStatus()
582
+ };
583
+ default:
584
+ throw new TTSError(
585
+ `Unknown tool: ${toolName}`,
586
+ "UNKNOWN" /* UNKNOWN */,
587
+ "tools"
588
+ );
80
589
  }
81
- });
82
- const ttsStatusTool = tool({
83
- description: '获取当前 TTS 播放状态',
84
- args: {},
85
- execute: async () => {
86
- const result = await handleToolCall('tts_status');
87
- if (result.success === false) {
88
- throw new Error(result.error || 'Unknown error');
590
+ } catch (error) {
591
+ if (error instanceof TTSError) {
592
+ return {
593
+ success: false,
594
+ error: error.message,
595
+ code: error.code,
596
+ provider: error.provider
597
+ };
598
+ }
599
+ return {
600
+ success: false,
601
+ error: String(error)
602
+ };
603
+ }
604
+ }
605
+
606
+ // src/providers/minimax.ts
607
+ import axios from "axios";
608
+ var MiniMaxProvider = class extends BaseTTSProvider {
609
+ name = "minimax";
610
+ capabilities = {
611
+ speak: true,
612
+ stream: true,
613
+ sync: true,
614
+ async: true,
615
+ voiceList: true,
616
+ voiceClone: true
617
+ };
618
+ config;
619
+ httpClient;
620
+ wsConnection;
621
+ currentAudioData = [];
622
+ audioFormat = "mp3";
623
+ constructor(config) {
624
+ super();
625
+ this.config = config;
626
+ this.apiKey = config.apiKey;
627
+ this.defaultVoice = config.voiceId;
628
+ this.defaultModel = config.model || "stream";
629
+ this.audioFormat = config.audioFormat || "mp3";
630
+ this.httpClient = axios.create({
631
+ baseURL: this.config.baseURL || "https://api.minimaxi.com",
632
+ headers: {
633
+ "Authorization": `Bearer ${this.apiKey}`,
634
+ "Content-Type": "application/json"
635
+ },
636
+ timeout: 3e4
637
+ });
638
+ }
639
+ async initialize() {
640
+ this.validateApiKey();
641
+ }
642
+ async destroy() {
643
+ if (this.wsConnection) {
644
+ this.wsConnection.close();
645
+ this.wsConnection = void 0;
646
+ }
647
+ }
648
+ async doSpeak(text, voice, model, options) {
649
+ this.validateApiKey();
650
+ switch (model) {
651
+ case "stream":
652
+ return this.streamingSpeak(text, voice, options);
653
+ case "sync":
654
+ return this.syncSpeak(text, voice, options);
655
+ case "async":
656
+ return this.asyncSpeak(text, voice, options);
657
+ default:
658
+ return this.streamingSpeak(text, voice, options);
659
+ }
660
+ }
661
+ /**
662
+ * 流式合成 (HTTP) - T2A v2 with stream: true
663
+ */
664
+ async streamingSpeak(text, voice, options) {
665
+ try {
666
+ const voiceId = voice || this.defaultVoice || "male-qn-qingse";
667
+ const speed = options?.speed || this.config.speed || 1;
668
+ const vol = options?.volume !== void 0 ? options.volume / 10 : this.config.volume !== void 0 ? this.config.volume / 10 : 1;
669
+ const pitch = options?.pitch !== void 0 ? Math.round((options.pitch - 1) * 12) : this.config.pitch !== void 0 ? Math.round((this.config.pitch - 1) * 12) : 0;
670
+ const response = await this.httpClient.post("/v1/t2a_v2", {
671
+ model: this.config.ttsModel || "speech-2.8-hd",
672
+ text,
673
+ stream: true,
674
+ voice_setting: {
675
+ voice_id: voiceId,
676
+ speed,
677
+ vol,
678
+ pitch
679
+ },
680
+ audio_setting: {
681
+ sample_rate: 32e3,
682
+ bitrate: 128e3,
683
+ format: this.audioFormat,
684
+ channel: 1
89
685
  }
90
- return result;
686
+ }, {
687
+ headers: {
688
+ "Authorization": `Bearer ${this.apiKey}`,
689
+ "Content-Type": "application/json"
690
+ },
691
+ responseType: "stream"
692
+ });
693
+ const stream = response.data;
694
+ const audioChunks = [];
695
+ return new Promise((resolve, reject) => {
696
+ stream.on("data", (chunk) => {
697
+ try {
698
+ const lines = chunk.toString().split("\n");
699
+ for (const line of lines) {
700
+ if (line.startsWith("data:")) {
701
+ const data = JSON.parse(line.slice(5));
702
+ if (data.data?.audio) {
703
+ audioChunks.push(Buffer.from(data.data.audio, "hex"));
704
+ }
705
+ if (data.data?.status === 2) {
706
+ const fullAudio = Buffer.concat(audioChunks);
707
+ resolve({
708
+ audioData: fullAudio,
709
+ format: this.audioFormat,
710
+ isStream: true,
711
+ duration: this.estimateDuration(fullAudio.length)
712
+ });
713
+ }
714
+ }
715
+ }
716
+ } catch (e) {
717
+ audioChunks.push(chunk);
718
+ }
719
+ });
720
+ stream.on("error", (err) => {
721
+ reject(new TTSError(
722
+ "Stream error",
723
+ "NETWORK" /* NETWORK */,
724
+ this.name,
725
+ err
726
+ ));
727
+ });
728
+ stream.on("end", () => {
729
+ if (audioChunks.length > 0) {
730
+ const fullAudio = Buffer.concat(audioChunks);
731
+ resolve({
732
+ audioData: fullAudio,
733
+ format: this.audioFormat,
734
+ isStream: true,
735
+ duration: this.estimateDuration(fullAudio.length)
736
+ });
737
+ }
738
+ });
739
+ });
740
+ } catch (error) {
741
+ if (error instanceof TTSError) throw error;
742
+ throw this.mapError(error);
91
743
  }
92
- });
93
- const ttsStreamSpeakTool = tool({
94
- description: '启动流式朗读(豆包模式),订阅AI回复并边生成边朗读',
95
- args: {
96
- text: tool.schema.string().optional().describe('初始文本(可选)'),
97
- voice: tool.schema.string().optional().describe('音色ID'),
98
- model: tool.schema.enum(['sync', 'async', 'stream']).optional().describe('合成模式')
99
- },
100
- execute: async (args) => {
101
- const result = await handleToolCall('tts_stream_speak', args);
102
- if (result.success === false) {
103
- throw new Error(result.error || 'Unknown error');
744
+ }
745
+ /**
746
+ * 同步合成 (HTTP) - T2A v2
747
+ * API: POST https://api.minimax.io/v1/t2a_v2
748
+ */
749
+ async syncSpeak(text, voice, options) {
750
+ try {
751
+ const voiceId = voice || this.defaultVoice || "male-qn-qingse";
752
+ const speed = options?.speed || this.config.speed || 1;
753
+ const vol = options?.volume !== void 0 ? options.volume / 10 : this.config.volume !== void 0 ? this.config.volume / 10 : 1;
754
+ const pitch = options?.pitch !== void 0 ? Math.round((options.pitch - 1) * 12) : this.config.pitch !== void 0 ? Math.round((this.config.pitch - 1) * 12) : 0;
755
+ const response = await this.httpClient.post("/v1/t2a_v2", {
756
+ model: this.config.ttsModel || "speech-2.8-hd",
757
+ text,
758
+ stream: false,
759
+ output_format: "hex",
760
+ voice_setting: {
761
+ voice_id: voiceId,
762
+ speed,
763
+ vol,
764
+ pitch
765
+ },
766
+ audio_setting: {
767
+ sample_rate: 32e3,
768
+ bitrate: 128e3,
769
+ format: this.audioFormat,
770
+ channel: 1
104
771
  }
105
- return result;
772
+ }, {
773
+ headers: {
774
+ "Authorization": `Bearer ${this.apiKey}`,
775
+ "Content-Type": "application/json"
776
+ }
777
+ });
778
+ if (response.data.base_resp?.status_code !== 0) {
779
+ throw new TTSError(
780
+ response.data.base_resp?.status_msg || "API request failed",
781
+ "UNKNOWN" /* UNKNOWN */,
782
+ this.name,
783
+ response.data.base_resp
784
+ );
785
+ }
786
+ const audioHex = response.data.data?.audio;
787
+ if (!audioHex) {
788
+ throw new TTSError(
789
+ "No audio data in response",
790
+ "UNKNOWN" /* UNKNOWN */,
791
+ this.name
792
+ );
793
+ }
794
+ const audioBuffer = Buffer.from(audioHex, "hex");
795
+ return {
796
+ audioData: audioBuffer,
797
+ format: this.audioFormat,
798
+ isStream: false,
799
+ duration: response.data.extra_info?.audio_length ? response.data.extra_info.audio_length / 1e3 : this.estimateDuration(audioBuffer.length)
800
+ };
801
+ } catch (error) {
802
+ if (error instanceof TTSError) throw error;
803
+ throw this.mapError(error);
106
804
  }
107
- });
108
- const ttsStreamStopTool = tool({
109
- description: '停止当前流式朗读',
110
- args: {},
111
- execute: async () => {
112
- const result = await handleToolCall('tts_stream_stop');
113
- if (result.success === false) {
114
- throw new Error(result.error || 'Unknown error');
805
+ }
806
+ /**
807
+ * 异步合成 (轮询) - T2A Async v2
808
+ */
809
+ async asyncSpeak(text, voice, options) {
810
+ try {
811
+ const voiceId = voice || this.defaultVoice || "male-qn-qingse";
812
+ const speed = options?.speed || this.config.speed || 1;
813
+ const vol = options?.volume !== void 0 ? options.volume / 10 : this.config.volume !== void 0 ? this.config.volume / 10 : 1;
814
+ const pitch = options?.pitch !== void 0 ? Math.round((options.pitch - 1) * 12) : this.config.pitch !== void 0 ? Math.round((this.config.pitch - 1) * 12) : 0;
815
+ const createResponse = await this.httpClient.post("/v1/t2a_async_v2", {
816
+ model: this.config.ttsModel || "speech-2.8-hd",
817
+ text,
818
+ voice_setting: {
819
+ voice_id: voiceId,
820
+ speed,
821
+ vol,
822
+ pitch
823
+ },
824
+ audio_setting: {
825
+ sample_rate: 32e3,
826
+ bitrate: 128e3,
827
+ format: this.audioFormat,
828
+ channel: 1
115
829
  }
116
- return result;
830
+ }, {
831
+ headers: {
832
+ "Authorization": `Bearer ${this.apiKey}`,
833
+ "Content-Type": "application/json"
834
+ }
835
+ });
836
+ const taskId = createResponse.data.task_id;
837
+ if (!taskId) {
838
+ throw new TTSError(
839
+ "No task_id in async response",
840
+ "UNKNOWN" /* UNKNOWN */,
841
+ this.name,
842
+ createResponse.data
843
+ );
844
+ }
845
+ let attempts = 0;
846
+ const maxAttempts = 60;
847
+ while (attempts < maxAttempts) {
848
+ await this.delay(2e3);
849
+ const statusResponse = await this.httpClient.get(
850
+ `/v1/query/t2a_async_query_v2?task_id=${taskId}`,
851
+ {
852
+ headers: {
853
+ "Authorization": `Bearer ${this.apiKey}`
854
+ }
855
+ }
856
+ );
857
+ if (statusResponse.data.status === "success") {
858
+ const fileId = statusResponse.data.file_id;
859
+ if (!fileId) {
860
+ throw new TTSError(
861
+ "No file_id in async response",
862
+ "UNKNOWN" /* UNKNOWN */,
863
+ this.name,
864
+ statusResponse.data
865
+ );
866
+ }
867
+ const downloadResponse = await this.httpClient.get(
868
+ `/v1/files/retrieve_content?file_id=${fileId}`,
869
+ {
870
+ headers: {
871
+ "Authorization": `Bearer ${this.apiKey}`
872
+ },
873
+ responseType: "arraybuffer"
874
+ }
875
+ );
876
+ return {
877
+ audioData: Buffer.from(downloadResponse.data),
878
+ format: this.audioFormat,
879
+ isStream: false,
880
+ duration: 0
881
+ };
882
+ }
883
+ if (statusResponse.data.status === "failed") {
884
+ throw new TTSError(
885
+ "Async TTS task failed",
886
+ "UNKNOWN" /* UNKNOWN */,
887
+ this.name,
888
+ statusResponse.data
889
+ );
890
+ }
891
+ attempts++;
892
+ }
893
+ throw new TTSError(
894
+ "Async TTS task timeout",
895
+ "NETWORK" /* NETWORK */,
896
+ this.name
897
+ );
898
+ } catch (error) {
899
+ if (error instanceof TTSError) throw error;
900
+ throw this.mapError(error);
117
901
  }
118
- });
119
- const ttsStreamStatusTool = tool({
120
- description: '获取当前流式朗读状态',
121
- args: {},
122
- execute: async () => {
123
- const result = await handleToolCall('tts_stream_status');
124
- if (result.success === false) {
125
- throw new Error(result.error || 'Unknown error');
902
+ }
903
+ /**
904
+ * 音色克隆 - 使用参考音频克隆声音
905
+ */
906
+ async voiceClone(audioUrl, text, voice) {
907
+ this.validateApiKey();
908
+ try {
909
+ const response = await this.httpClient.post("/v1/t2a_v2/voice_clone", {
910
+ audio_url: audioUrl,
911
+ text,
912
+ voice_id: voice || "custom_clone"
913
+ }, {
914
+ headers: {
915
+ "Authorization": `Bearer ${this.apiKey}`,
916
+ "Content-Type": "application/json"
917
+ },
918
+ responseType: "arraybuffer"
919
+ });
920
+ return {
921
+ audioData: Buffer.from(response.data),
922
+ format: this.audioFormat,
923
+ isStream: false,
924
+ duration: this.estimateDuration(response.data.length)
925
+ };
926
+ } catch (error) {
927
+ throw this.mapError(error);
928
+ }
929
+ }
930
+ /**
931
+ * 获取音色列表
932
+ */
933
+ async listVoices() {
934
+ this.validateApiKey();
935
+ try {
936
+ const response = await this.httpClient.get("/v1/t2a/voices", {
937
+ headers: {
938
+ "Authorization": `Bearer ${this.apiKey}`
126
939
  }
127
- return typeof result === 'string' ? result : JSON.stringify(result);
940
+ });
941
+ return response.data.voices.map((v) => ({
942
+ id: v.voice_id,
943
+ name: v.name,
944
+ language: v.language,
945
+ gender: v.gender
946
+ }));
947
+ } catch (error) {
948
+ return MINIMAX_VOICES;
128
949
  }
129
- });
130
- const OcosayPlugin = async (_input, _options) => {
131
- console.info(`${pluginName}: initializing...`);
132
- const config = loadOrCreateConfig();
133
- await initialize({
134
- autoRead: config.autoRead,
135
- providers: {
136
- minimax: {
137
- apiKey: config.providers.minimax.apiKey,
138
- baseURL: config.providers.minimax.baseURL || undefined,
139
- voiceId: config.providers.minimax.voiceId || undefined
140
- }
950
+ }
951
+ /**
952
+ * 错误映射
953
+ */
954
+ mapError(error) {
955
+ if (error.response) {
956
+ const status = error.response.status;
957
+ const code = status === 401 ? "AUTH" /* AUTH */ : status === 429 ? "QUOTA" /* QUOTA */ : status >= 500 ? "NETWORK" /* NETWORK */ : "UNKNOWN" /* UNKNOWN */;
958
+ return new TTSError(
959
+ error.response.data?.message || "API request failed",
960
+ code,
961
+ this.name,
962
+ error.response.data
963
+ );
964
+ }
965
+ if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
966
+ return new TTSError(
967
+ "Network error: Unable to connect to MiniMax API",
968
+ "NETWORK" /* NETWORK */,
969
+ this.name,
970
+ error.message
971
+ );
972
+ }
973
+ return new TTSError(
974
+ error.message || "Unknown error",
975
+ "UNKNOWN" /* UNKNOWN */,
976
+ this.name,
977
+ error
978
+ );
979
+ }
980
+ /**
981
+ * 延迟辅助函数
982
+ */
983
+ delay(ms) {
984
+ return new Promise((resolve) => setTimeout(resolve, ms));
985
+ }
986
+ /**
987
+ * 估算音频时长
988
+ * 基于 32kbps MP3 估算
989
+ */
990
+ estimateDuration(bytes) {
991
+ return bytes * 8 / (32e3 * 60);
992
+ }
993
+ };
994
+ var MINIMAX_VOICES = [
995
+ { id: "male-qn-qingse", name: "\u9752\u5E74\u6E05\u6F88", language: "zh-CN", gender: "male" },
996
+ { id: "male-qn-qingse_2", name: "\u9752\u5E74\u6E05\u6F88v2", language: "zh-CN", gender: "male" },
997
+ { id: "female-shaonv", name: "\u5C11\u5973", language: "zh-CN", gender: "female" },
998
+ { id: "male-baiming", name: "\u6210\u719F\u7537\u58F0", language: "zh-CN", gender: "male" },
999
+ { id: "female-tianmei", name: "\u751C\u7F8E\u5973\u58F0", language: "zh-CN", gender: "female" },
1000
+ { id: "male-zhongnan", name: "\u4E2D\u5E74\u7537\u58F0", language: "zh-CN", gender: "male" },
1001
+ { id: "female-yujie", name: "\u5FA1\u59D0\u97F3", language: "zh-CN", gender: "female" },
1002
+ { id: "male-qn-xiaoao", name: "\u9752\u5E74\u8C6A\u723D", language: "zh-CN", gender: "male" },
1003
+ { id: "female-shandian", name: "\u751C\u5FC3\u5C0F\u5A18", language: "zh-CN", gender: "female" },
1004
+ { id: "male-qn-buke", name: "\u9752\u5E74\u4F4E\u6C89", language: "zh-CN", gender: "male" },
1005
+ { id: "male-qn-wenlv", name: "\u6587\u7EFF\u9752\u5E74", language: "zh-CN", gender: "male" },
1006
+ { id: "female-tianmei-2", name: "\u751C\u7F8E\u5973\u58F0v2", language: "zh-CN", gender: "female" },
1007
+ { id: "female-yujie-2", name: "\u5FA1\u59D0\u97F3v2", language: "zh-CN", gender: "female" },
1008
+ { id: "male-shaonian", name: "\u5C11\u5E74\u97F3", language: "zh-CN", gender: "male" },
1009
+ { id: "female-yunv", name: "\u6E29\u67D4\u5973\u58F0", language: "zh-CN", gender: "female" },
1010
+ { id: "male-qn-jingdian", name: "\u7ECF\u5178\u7537\u58F0", language: "zh-CN", gender: "male" },
1011
+ { id: "male-qn-kuang\u91CE", name: "\u72C2\u91CE\u9752\u5E74", language: "zh-CN", gender: "male" },
1012
+ { id: "female-yujie-old", name: "\u4F18\u96C5\u4F4E\u6C89", language: "zh-CN", gender: "female" },
1013
+ { id: "female-tianmei-old", name: "\u751C\u7F8E\u5973\u5B69", language: "zh-CN", gender: "female" },
1014
+ { id: "male-qn-taohua", name: "\u6843\u82B1\u9752\u5E74", language: "zh-CN", gender: "male" }
1015
+ ];
1016
+
1017
+ // src/core/stream-reader.ts
1018
+ import { EventEmitter as EventEmitter3 } from "events";
1019
+ var StreamReader = class extends EventEmitter3 {
1020
+ constructor(bufferSize = 30, bufferTimeout = 2e3) {
1021
+ super();
1022
+ this.bufferSize = bufferSize;
1023
+ this.bufferTimeout = bufferTimeout;
1024
+ }
1025
+ bufferSize;
1026
+ bufferTimeout;
1027
+ state = "idle" /* IDLE */;
1028
+ buffer = "";
1029
+ sessionID;
1030
+ messageID;
1031
+ partID;
1032
+ timeoutHandle;
1033
+ /**
1034
+ * 启动流式监听
1035
+ * 将状态从 IDLE 切换到 BUFFERING,开始监听事件
1036
+ */
1037
+ start() {
1038
+ if (this.state === "idle" /* IDLE */) {
1039
+ this.state = "buffering" /* BUFFERING */;
1040
+ this.emit("streamStart");
1041
+ }
1042
+ }
1043
+ /**
1044
+ * 处理 message.part.delta 事件
1045
+ */
1046
+ handleDelta(sessionID, messageID, partID, delta) {
1047
+ if (this.state === "idle" /* IDLE */) {
1048
+ this.state = "buffering" /* BUFFERING */;
1049
+ this.sessionID = sessionID;
1050
+ this.messageID = messageID;
1051
+ this.partID = partID;
1052
+ this.emit("streamStart");
1053
+ }
1054
+ this.buffer += delta;
1055
+ this.resetTimeout();
1056
+ if (this.shouldFlush()) {
1057
+ this.flushBuffer();
1058
+ }
1059
+ }
1060
+ /**
1061
+ * 处理流结束
1062
+ */
1063
+ handleEnd() {
1064
+ if (this.state === "ended" /* ENDED */) {
1065
+ return;
1066
+ }
1067
+ if (this.buffer.length > 0) {
1068
+ this.flushBuffer();
1069
+ }
1070
+ this.state = "ended" /* ENDED */;
1071
+ this.clearTimeout();
1072
+ this.emit("streamEnd");
1073
+ }
1074
+ /**
1075
+ * 处理错误
1076
+ */
1077
+ handleError(error) {
1078
+ this.clearTimeout();
1079
+ this.state = "idle" /* IDLE */;
1080
+ this.buffer = "";
1081
+ this.emit("streamError", error);
1082
+ }
1083
+ /**
1084
+ * 重置缓冲器
1085
+ */
1086
+ reset() {
1087
+ this.state = "idle" /* IDLE */;
1088
+ this.buffer = "";
1089
+ this.sessionID = void 0;
1090
+ this.messageID = void 0;
1091
+ this.partID = void 0;
1092
+ this.clearTimeout();
1093
+ }
1094
+ /**
1095
+ * 判断是否应该刷新缓冲区
1096
+ * 条件:
1097
+ * 1. 包含句子结束符(任何长度)
1098
+ * 2. 缓冲区长度 >= bufferSize
1099
+ */
1100
+ shouldFlush() {
1101
+ const sentenceEnd = /[。!?.!?]|……/;
1102
+ if (sentenceEnd.test(this.buffer)) {
1103
+ return true;
1104
+ }
1105
+ if (this.buffer.length >= this.bufferSize) {
1106
+ return true;
1107
+ }
1108
+ return false;
1109
+ }
1110
+ /**
1111
+ * 刷新缓冲区,发送textReady事件
1112
+ */
1113
+ flushBuffer() {
1114
+ const text = this.buffer.trim();
1115
+ if (text.length > 0) {
1116
+ this.emit("textReady", text);
1117
+ }
1118
+ this.buffer = "";
1119
+ this.resetTimeout();
1120
+ }
1121
+ /**
1122
+ * 重置超时计时器
1123
+ */
1124
+ resetTimeout() {
1125
+ this.clearTimeout();
1126
+ this.timeoutHandle = setTimeout(() => {
1127
+ if (this.buffer.length > 0) {
1128
+ this.flushBuffer();
1129
+ }
1130
+ }, this.bufferTimeout);
1131
+ }
1132
+ /**
1133
+ * 清除超时计时器
1134
+ */
1135
+ clearTimeout() {
1136
+ if (this.timeoutHandle) {
1137
+ clearTimeout(this.timeoutHandle);
1138
+ this.timeoutHandle = void 0;
1139
+ }
1140
+ }
1141
+ /**
1142
+ * 获取当前状态
1143
+ */
1144
+ getState() {
1145
+ return this.state;
1146
+ }
1147
+ /**
1148
+ * 检查流是否处于活跃状态
1149
+ */
1150
+ isActive() {
1151
+ return this.state === "buffering" /* BUFFERING */;
1152
+ }
1153
+ /**
1154
+ * 获取当前缓冲区内容
1155
+ */
1156
+ getBuffer() {
1157
+ return this.buffer;
1158
+ }
1159
+ /**
1160
+ * 获取当前会话ID
1161
+ */
1162
+ getSessionID() {
1163
+ return this.sessionID;
1164
+ }
1165
+ /**
1166
+ * 获取当前消息ID
1167
+ */
1168
+ getMessageID() {
1169
+ return this.messageID;
1170
+ }
1171
+ /**
1172
+ * 获取当前分块ID
1173
+ */
1174
+ getPartID() {
1175
+ return this.partID;
1176
+ }
1177
+ };
1178
+
1179
+ // src/core/streaming-synthesizer.ts
1180
+ import { EventEmitter as EventEmitter4 } from "events";
1181
+ var StreamingSynthesizer = class extends EventEmitter4 {
1182
+ constructor(options) {
1183
+ super();
1184
+ this.options = options;
1185
+ }
1186
+ options;
1187
+ audioChunks = [];
1188
+ /**
1189
+ * 发送文本片段进行合成
1190
+ * 调用 provider.speak() 并处理返回的音频流
1191
+ */
1192
+ async synthesize(text) {
1193
+ if (!text || text.trim().length === 0) {
1194
+ return;
1195
+ }
1196
+ try {
1197
+ const result = await this.options.provider.speak(text, {
1198
+ model: "stream",
1199
+ voice: this.options.voice,
1200
+ speed: this.options.speed,
1201
+ volume: this.options.volume,
1202
+ pitch: this.options.pitch
1203
+ });
1204
+ await this.processAudioResult(result);
1205
+ this.emit("done");
1206
+ } catch (error) {
1207
+ const ttsError = error instanceof TTSError ? error : new TTSError(
1208
+ error instanceof Error ? error.message : "Synthesis failed",
1209
+ "UNKNOWN",
1210
+ this.options.provider.name,
1211
+ error
1212
+ );
1213
+ this.emit("error", ttsError);
1214
+ }
1215
+ }
1216
+ /**
1217
+ * 处理 AudioResult,根据 audioData 类型进行相应处理
1218
+ */
1219
+ async processAudioResult(result) {
1220
+ if (result.isStream && result.audioData instanceof ReadableStream) {
1221
+ await this.processReadableStream(result.audioData);
1222
+ } else if (Buffer.isBuffer(result.audioData)) {
1223
+ this.emitChunk(result.audioData);
1224
+ }
1225
+ }
1226
+ /**
1227
+ * 处理 ReadableStream,逐chunk emit
1228
+ */
1229
+ async processReadableStream(stream) {
1230
+ const reader = stream.getReader();
1231
+ try {
1232
+ while (true) {
1233
+ const { done, value } = await reader.read();
1234
+ if (done) {
1235
+ break;
141
1236
  }
1237
+ if (value) {
1238
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
1239
+ this.emitChunk(chunk);
1240
+ }
1241
+ }
1242
+ } finally {
1243
+ reader.releaseLock();
1244
+ }
1245
+ }
1246
+ /**
1247
+ * emit chunk 并累积
1248
+ */
1249
+ emitChunk(chunk) {
1250
+ this.audioChunks.push(chunk);
1251
+ this.emit("chunk", chunk);
1252
+ }
1253
+ /**
1254
+ * 重置状态
1255
+ * 清空累积的音频数据
1256
+ */
1257
+ reset() {
1258
+ this.audioChunks = [];
1259
+ }
1260
+ /**
1261
+ * 获取累积的音频数据
1262
+ * 返回所有已接收的 chunk
1263
+ */
1264
+ getAudioChunks() {
1265
+ return [...this.audioChunks];
1266
+ }
1267
+ };
1268
+
1269
+ // src/core/stream-player.ts
1270
+ import { EventEmitter as EventEmitter5 } from "events";
1271
+ import { spawn as spawn2 } from "child_process";
1272
+ import fs2 from "fs";
1273
+ import { createWriteStream as createWriteStream2 } from "fs";
1274
+ import { tmpdir as tmpdir2 } from "os";
1275
+ import { join as join2 } from "path";
1276
+ var StreamPlayer = class extends EventEmitter5 {
1277
+ tempFile = "";
1278
+ writeStream;
1279
+ playerProcess;
1280
+ _bytesWritten = 0;
1281
+ _started = false;
1282
+ _paused = false;
1283
+ _stopped = false;
1284
+ format = "mp3";
1285
+ events;
1286
+ constructor(options = {}) {
1287
+ super();
1288
+ this.format = options.format || "mp3";
1289
+ this.events = options.events;
1290
+ }
1291
+ /**
1292
+ * 开始播放
1293
+ * 创建临时文件,创建写入流,启动播放器进程
1294
+ */
1295
+ start() {
1296
+ if (this._started) {
1297
+ return;
1298
+ }
1299
+ this.tempFile = join2(tmpdir2(), `ocosay-stream-${Date.now()}.${this.format}`);
1300
+ this.writeStream = createWriteStream2(this.tempFile, { highWaterMark: 64 * 1024 });
1301
+ this.writeStream.on("error", (error) => {
1302
+ this.handleError(error);
1303
+ });
1304
+ this.writeStream.on("finish", () => {
1305
+ });
1306
+ this.startPlayer();
1307
+ this._started = true;
1308
+ this._stopped = false;
1309
+ this.events?.onStart?.();
1310
+ this.emit("start");
1311
+ }
1312
+ /**
1313
+ * 启动播放器进程
1314
+ */
1315
+ startPlayer() {
1316
+ const platform = process.platform;
1317
+ let command;
1318
+ let args;
1319
+ if (platform === "darwin") {
1320
+ command = "afplay";
1321
+ args = [this.tempFile];
1322
+ } else if (platform === "linux") {
1323
+ command = "aplay";
1324
+ args = [this.tempFile];
1325
+ } else {
1326
+ this.handleError(new Error("Windows platform is not supported for stream playback. PlaySync() blocks the Node.js event loop."));
1327
+ return;
1328
+ }
1329
+ this.playerProcess = spawn2(command, args, {
1330
+ stdio: "ignore",
1331
+ detached: false
1332
+ });
1333
+ this.playerProcess.on("exit", (code, signal) => {
1334
+ if (this._stopped) {
1335
+ return;
1336
+ }
1337
+ if (signal === "SIGTERM" || signal === "SIGINT") {
1338
+ return;
1339
+ }
1340
+ if (code === 0 || code === null) {
1341
+ this.events?.onEnd?.();
1342
+ this.emit("end");
1343
+ } else {
1344
+ this.handleError(new Error(`Player exited with code ${code}`));
1345
+ }
1346
+ });
1347
+ this.playerProcess.on("error", (error) => {
1348
+ this.handleError(error);
142
1349
  });
1350
+ }
1351
+ /**
1352
+ * 写入音频数据块(边收边写)
1353
+ * 如果尚未 start(),会自动调用
1354
+ */
1355
+ write(chunk) {
1356
+ if (this._stopped) {
1357
+ return;
1358
+ }
1359
+ if (!this._started) {
1360
+ this.start();
1361
+ }
1362
+ if (this.writeStream) {
1363
+ const canContinue = this.writeStream.write(chunk);
1364
+ if (!canContinue) {
1365
+ this.writeStream.once("drain", () => {
1366
+ });
1367
+ }
1368
+ this._bytesWritten += chunk.length;
1369
+ this.events?.onProgress?.(this._bytesWritten);
1370
+ this.emit("progress", this._bytesWritten);
1371
+ }
1372
+ }
1373
+ /**
1374
+ * 结束写入
1375
+ * 关闭写入流,但不杀播放器进程,让它播完
1376
+ */
1377
+ end() {
1378
+ if (this.writeStream) {
1379
+ this.writeStream.end();
1380
+ this.writeStream = void 0;
1381
+ }
1382
+ }
1383
+ /**
1384
+ * 停止播放
1385
+ * 杀死播放器进程,删除临时文件
1386
+ */
1387
+ stop() {
1388
+ this._stopped = true;
1389
+ this._started = false;
1390
+ this._paused = false;
1391
+ if (this.playerProcess) {
1392
+ try {
1393
+ this.playerProcess.kill("SIGTERM");
1394
+ } catch (e) {
1395
+ }
1396
+ this.playerProcess = void 0;
1397
+ }
1398
+ if (this.writeStream) {
1399
+ try {
1400
+ this.writeStream.destroy();
1401
+ } catch (e) {
1402
+ }
1403
+ this.writeStream = void 0;
1404
+ }
1405
+ this.deleteTempFile();
1406
+ this.events?.onStop?.();
1407
+ this.emit("stop");
1408
+ }
1409
+ /**
1410
+ * 暂停播放
1411
+ * 使用 SIGSTOP 暂停播放器进程
1412
+ */
1413
+ pause() {
1414
+ if (!this._started || this._paused || this._stopped) {
1415
+ return;
1416
+ }
1417
+ if (this.playerProcess) {
1418
+ try {
1419
+ this.playerProcess.kill("SIGSTOP");
1420
+ this._paused = true;
1421
+ this.events?.onPause?.();
1422
+ this.emit("pause");
1423
+ } catch (e) {
1424
+ }
1425
+ }
1426
+ }
1427
+ /**
1428
+ * 恢复播放
1429
+ */
1430
+ resume() {
1431
+ if (!this._paused || this._stopped) {
1432
+ return;
1433
+ }
1434
+ if (this.playerProcess) {
1435
+ try {
1436
+ this.playerProcess.kill("SIGCONT");
1437
+ this._paused = false;
1438
+ this.events?.onResume?.();
1439
+ this.emit("resume");
1440
+ } catch (e) {
1441
+ }
1442
+ }
1443
+ }
1444
+ /**
1445
+ * 是否已启动
1446
+ */
1447
+ isStarted() {
1448
+ return this._started;
1449
+ }
1450
+ /**
1451
+ * 是否暂停
1452
+ */
1453
+ isPaused() {
1454
+ return this._paused;
1455
+ }
1456
+ /**
1457
+ * 是否已停止
1458
+ */
1459
+ isStopped() {
1460
+ return this._stopped;
1461
+ }
1462
+ /**
1463
+ * 获取已写入的字节数
1464
+ */
1465
+ getBytesWritten() {
1466
+ return this._bytesWritten;
1467
+ }
1468
+ /**
1469
+ * 获取临时文件路径
1470
+ */
1471
+ getTempFile() {
1472
+ return this.tempFile;
1473
+ }
1474
+ /**
1475
+ * 处理错误
1476
+ */
1477
+ handleError(error) {
1478
+ this.events?.onError?.(error);
1479
+ this.emit("error", error);
1480
+ }
1481
+ /**
1482
+ * 删除临时文件
1483
+ */
1484
+ deleteTempFile() {
1485
+ if (this.tempFile) {
1486
+ try {
1487
+ if (fs2.existsSync(this.tempFile)) {
1488
+ fs2.unlinkSync(this.tempFile);
1489
+ }
1490
+ } catch (e) {
1491
+ }
1492
+ this.tempFile = "";
1493
+ }
1494
+ }
1495
+ };
1496
+
1497
+ // src/index.ts
1498
+ var speaker;
1499
+ var streamReader;
1500
+ var streamingSynthesizer;
1501
+ var streamPlayer;
1502
+ var initialized = false;
1503
+ var autoReadEnabled = false;
1504
+ async function initialize(config) {
1505
+ if (initialized) {
1506
+ return;
1507
+ }
1508
+ if (config?.providers?.minimax) {
1509
+ if (!config.providers.minimax.apiKey) {
1510
+ throw new Error("[ocosay] API Key is required. Please set minimax.apiKey in ~/.config/opencode/ocosay.jsonc");
1511
+ }
1512
+ const minimaxProvider = new MiniMaxProvider(config.providers.minimax);
1513
+ registerProvider("minimax", minimaxProvider);
1514
+ await minimaxProvider.initialize();
1515
+ }
1516
+ const speakerOptions = {
1517
+ defaultProvider: config?.defaultProvider || "minimax",
1518
+ defaultModel: config?.defaultModel || "stream",
1519
+ defaultVoice: config?.defaultVoice
1520
+ };
1521
+ speaker = new Speaker(speakerOptions);
1522
+ if (config?.autoRead) {
1523
+ autoReadEnabled = true;
1524
+ initializeStreamComponents(config);
1525
+ }
1526
+ initialized = true;
1527
+ }
1528
+ function initializeStreamComponents(config) {
1529
+ const provider = getProvider(config?.defaultProvider || "minimax");
1530
+ streamReader = new StreamReader(
1531
+ config?.streamBufferSize || 30,
1532
+ config?.streamBufferTimeout || 2e3
1533
+ );
1534
+ streamingSynthesizer = new StreamingSynthesizer({
1535
+ provider,
1536
+ voice: config?.defaultVoice,
1537
+ speed: 1,
1538
+ volume: 1,
1539
+ pitch: 1
1540
+ });
1541
+ const playerEvents = {
1542
+ onStart: () => {
1543
+ },
1544
+ onEnd: () => {
1545
+ },
1546
+ onProgress: (bytesWritten) => {
1547
+ },
1548
+ onError: (error) => console.error("Stream player error:", error),
1549
+ onStop: () => {
1550
+ }
1551
+ };
1552
+ streamPlayer = new StreamPlayer({ events: playerEvents });
1553
+ const synthesisQueue = [];
1554
+ let isSynthesizing = false;
1555
+ async function processQueue() {
1556
+ while (synthesisQueue.length > 0) {
1557
+ const text = synthesisQueue.shift();
1558
+ isSynthesizing = true;
1559
+ try {
1560
+ await streamingSynthesizer?.synthesize(text);
1561
+ } catch (error) {
1562
+ console.error("Synthesize error:", error);
1563
+ }
1564
+ }
1565
+ isSynthesizing = false;
1566
+ }
1567
+ streamReader.on("textReady", (text) => {
1568
+ synthesisQueue.push(text);
1569
+ if (!isSynthesizing) {
1570
+ processQueue();
1571
+ }
1572
+ });
1573
+ streamingSynthesizer.on("chunk", (chunk) => {
1574
+ if (streamPlayer) {
1575
+ streamPlayer.write(chunk);
1576
+ }
1577
+ });
1578
+ streamingSynthesizer.on("done", () => {
1579
+ if (streamPlayer) {
1580
+ streamPlayer.end();
1581
+ }
1582
+ });
1583
+ const TuiEventBus = global.__opencode_tuieventbus__;
1584
+ if (TuiEventBus) {
1585
+ const eventBus = new TuiEventBus();
1586
+ let messagePartDeltaHandler;
1587
+ let messagePartEndHandler;
1588
+ messagePartDeltaHandler = (event) => {
1589
+ if (event?.properties) {
1590
+ streamReader?.handleDelta(
1591
+ event.sessionId || "",
1592
+ event.messageId || "",
1593
+ event.partId || "",
1594
+ event.properties.delta || ""
1595
+ );
1596
+ }
1597
+ };
1598
+ messagePartEndHandler = () => {
1599
+ streamReader?.handleEnd();
1600
+ };
1601
+ eventBus.on("message.part.delta", messagePartDeltaHandler);
1602
+ eventBus.on("message.part.end", messagePartEndHandler);
1603
+ }
1604
+ }
1605
+ function isStreamEnabled() {
1606
+ return streamReader !== void 0 && streamingSynthesizer !== void 0 && streamPlayer !== void 0;
1607
+ }
1608
+ function isAutoReadEnabled() {
1609
+ return autoReadEnabled;
1610
+ }
1611
+ function getStreamStatus() {
1612
+ if (!streamReader) {
1613
+ return { isActive: false, bytesWritten: 0, state: "not_initialized" };
1614
+ }
1615
+ return {
1616
+ isActive: streamReader.isActive(),
1617
+ bytesWritten: streamPlayer?.getBytesWritten() ?? 0,
1618
+ state: streamReader.getState()
1619
+ };
1620
+ }
1621
+ function getStreamReader() {
1622
+ return streamReader;
1623
+ }
1624
+ function getStreamingSynthesizer() {
1625
+ return streamingSynthesizer;
1626
+ }
1627
+ function getStreamPlayer() {
1628
+ return streamPlayer;
1629
+ }
1630
+
1631
+ // src/config.ts
1632
+ import * as fs3 from "fs";
1633
+ import * as path from "path";
1634
+ import * as os from "os";
1635
+ var DEFAULT_CONFIG = {
1636
+ enabled: true,
1637
+ autoPlay: false,
1638
+ autoRead: false,
1639
+ streamMode: true,
1640
+ streamBufferSize: 30,
1641
+ streamBufferTimeout: 2e3,
1642
+ speed: 1,
1643
+ volume: 80,
1644
+ pitch: 1
1645
+ };
1646
+ var CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "ocosay.jsonc");
1647
+ function generateDefaultConfig() {
1648
+ return {
1649
+ ...DEFAULT_CONFIG,
1650
+ providers: {
1651
+ minimax: {
1652
+ apiKey: "",
1653
+ baseURL: "https://api.minimaxi.com",
1654
+ voiceId: "female-chengshu",
1655
+ model: "stream",
1656
+ ttsModel: "speech-2.8-hd",
1657
+ audioFormat: "mp3"
1658
+ }
1659
+ }
1660
+ };
1661
+ }
1662
+ function stripComments(jsonc) {
1663
+ let result = "";
1664
+ let inString = false;
1665
+ let stringChar = "";
1666
+ let i = 0;
1667
+ while (i < jsonc.length) {
1668
+ const char = jsonc[i];
1669
+ if (!inString && (char === '"' || char === "'" || char === "`")) {
1670
+ inString = true;
1671
+ stringChar = char;
1672
+ result += char;
1673
+ i++;
1674
+ continue;
1675
+ }
1676
+ if (inString && char === stringChar && jsonc[i - 1] !== "\\") {
1677
+ inString = false;
1678
+ result += char;
1679
+ i++;
1680
+ continue;
1681
+ }
1682
+ if (inString) {
1683
+ result += char;
1684
+ i++;
1685
+ continue;
1686
+ }
1687
+ if (char === "/" && jsonc[i + 1] === "/") {
1688
+ while (i < jsonc.length && jsonc[i] !== "\n") {
1689
+ i++;
1690
+ }
1691
+ continue;
1692
+ }
1693
+ if (char === "/" && jsonc[i + 1] === "*") {
1694
+ i += 2;
1695
+ while (i < jsonc.length - 1 && !(jsonc[i] === "*" && jsonc[i + 1] === "/")) {
1696
+ i++;
1697
+ }
1698
+ i += 2;
1699
+ continue;
1700
+ }
1701
+ result += char;
1702
+ i++;
1703
+ }
1704
+ return result;
1705
+ }
1706
+ function mergeWithDefaults(loaded, defaults) {
1707
+ return {
1708
+ enabled: loaded.enabled ?? defaults.enabled,
1709
+ autoPlay: loaded.autoPlay ?? defaults.autoPlay,
1710
+ autoRead: loaded.autoRead ?? defaults.autoRead,
1711
+ streamMode: loaded.streamMode ?? defaults.streamMode,
1712
+ streamBufferSize: loaded.streamBufferSize ?? defaults.streamBufferSize,
1713
+ streamBufferTimeout: loaded.streamBufferTimeout ?? defaults.streamBufferTimeout,
1714
+ speed: loaded.speed ?? defaults.speed,
1715
+ volume: loaded.volume ?? defaults.volume,
1716
+ pitch: loaded.pitch ?? defaults.pitch
1717
+ };
1718
+ }
1719
+ function loadOrCreateConfig() {
1720
+ const configDir = path.dirname(CONFIG_PATH);
1721
+ if (!fs3.existsSync(configDir)) {
1722
+ try {
1723
+ fs3.mkdirSync(configDir, { recursive: true });
1724
+ } catch (err) {
1725
+ throw new Error(`[ocosay] \u65E0\u6CD5\u521B\u5EFA\u914D\u7F6E\u76EE\u5F55 ${configDir}: ${err}`);
1726
+ }
1727
+ }
1728
+ if (!fs3.existsSync(CONFIG_PATH)) {
1729
+ console.info("[ocosay] \u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF0C\u6B63\u5728\u521B\u5EFA\u9ED8\u8BA4\u914D\u7F6E...");
1730
+ const defaultConfig = generateDefaultConfig();
1731
+ const configContent = JSON.stringify(defaultConfig, null, 2);
1732
+ try {
1733
+ fs3.writeFileSync(CONFIG_PATH, configContent, "utf-8");
1734
+ try {
1735
+ fs3.chmodSync(CONFIG_PATH, 384);
1736
+ } catch (err) {
1737
+ console.warn(`[ocosay] \u65E0\u6CD5\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\u6743\u9650: ${err}`);
1738
+ }
1739
+ } catch (err) {
1740
+ throw new Error(`[ocosay] \u65E0\u6CD5\u5199\u5165\u914D\u7F6E\u6587\u4EF6 ${CONFIG_PATH}: ${err}`);
1741
+ }
1742
+ console.info(`[ocosay] \u914D\u7F6E\u6587\u4EF6\u5DF2\u521B\u5EFA: ${CONFIG_PATH}`);
1743
+ console.info("[ocosay] \u8BF7\u7F16\u8F91\u914D\u7F6E\u6587\u4EF6\u586B\u5165 API Key \u548C Base URL");
1744
+ return defaultConfig;
1745
+ }
1746
+ try {
1747
+ const content = fs3.readFileSync(CONFIG_PATH, "utf-8");
1748
+ const stripped = stripComments(content);
1749
+ const loaded = JSON.parse(stripped);
1750
+ const merged = mergeWithDefaults(loaded, DEFAULT_CONFIG);
143
1751
  return {
144
- tool: {
145
- tts_speak: ttsSpeakTool,
146
- tts_stop: ttsStopTool,
147
- tts_pause: ttsPauseTool,
148
- tts_resume: ttsResumeTool,
149
- tts_list_voices: ttsListVoicesTool,
150
- tts_list_providers: ttsListProvidersTool,
151
- tts_status: ttsStatusTool,
152
- tts_stream_speak: ttsStreamSpeakTool,
153
- tts_stream_stop: ttsStreamStopTool,
154
- tts_stream_status: ttsStreamStatusTool
155
- },
156
- config: async () => {
157
- return;
1752
+ ...merged,
1753
+ providers: {
1754
+ minimax: {
1755
+ apiKey: loaded.providers?.minimax?.apiKey ?? "",
1756
+ baseURL: loaded.providers?.minimax?.baseURL ?? "",
1757
+ voiceId: loaded.providers?.minimax?.voiceId ?? "",
1758
+ model: loaded.providers?.minimax?.model ?? "stream",
1759
+ ttsModel: loaded.providers?.minimax?.ttsModel ?? "speech-2.8-hd",
1760
+ audioFormat: loaded.providers?.minimax?.audioFormat ?? "mp3"
158
1761
  }
1762
+ }
159
1763
  };
1764
+ } catch (error) {
1765
+ console.error("[ocosay] \u914D\u7F6E\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E:", error);
1766
+ return generateDefaultConfig();
1767
+ }
1768
+ }
1769
+
1770
+ // src/plugin.ts
1771
+ var pluginName = "ocosay";
1772
+ var ttsSpeakTool = tool({
1773
+ description: "\u5C06\u6587\u672C\u8F6C\u6362\u4E3A\u8BED\u97F3\u5E76\u64AD\u653E",
1774
+ args: {
1775
+ text: tool.schema.string().describe("\u8981\u8F6C\u6362\u7684\u6587\u672C\u5185\u5BB9"),
1776
+ provider: tool.schema.string().optional().describe("TTS \u63D0\u4F9B\u5546\u540D\u79F0"),
1777
+ voice: tool.schema.string().optional().describe("\u97F3\u8272 ID"),
1778
+ model: tool.schema.enum(["sync", "async", "stream"]).optional().describe("\u5408\u6210\u6A21\u5F0F"),
1779
+ speed: tool.schema.number().optional().describe("\u8BED\u901F 0.5-2.0"),
1780
+ volume: tool.schema.number().optional().describe("\u97F3\u91CF 0-100"),
1781
+ pitch: tool.schema.number().optional().describe("\u97F3\u8C03 0.5-2.0")
1782
+ },
1783
+ execute: async (args) => {
1784
+ const result = await handleToolCall("tts_speak", args);
1785
+ if (result.success === false) {
1786
+ throw new Error(result.error || "Unknown error");
1787
+ }
1788
+ return result;
1789
+ }
1790
+ });
1791
+ var ttsStopTool = tool({
1792
+ description: "\u505C\u6B62\u5F53\u524D TTS \u64AD\u653E",
1793
+ args: {},
1794
+ execute: async () => {
1795
+ const result = await handleToolCall("tts_stop");
1796
+ if (result.success === false) {
1797
+ throw new Error(result.error || "Unknown error");
1798
+ }
1799
+ return result;
1800
+ }
1801
+ });
1802
+ var ttsPauseTool = tool({
1803
+ description: "\u6682\u505C\u5F53\u524D TTS \u64AD\u653E",
1804
+ args: {},
1805
+ execute: async () => {
1806
+ const result = await handleToolCall("tts_pause");
1807
+ if (result.success === false) {
1808
+ throw new Error(result.error || "Unknown error");
1809
+ }
1810
+ return result;
1811
+ }
1812
+ });
1813
+ var ttsResumeTool = tool({
1814
+ description: "\u6062\u590D\u6682\u505C\u7684 TTS \u64AD\u653E",
1815
+ args: {},
1816
+ execute: async () => {
1817
+ const result = await handleToolCall("tts_resume");
1818
+ if (result.success === false) {
1819
+ throw new Error(result.error || "Unknown error");
1820
+ }
1821
+ return result;
1822
+ }
1823
+ });
1824
+ var ttsListVoicesTool = tool({
1825
+ description: "\u5217\u51FA\u53EF\u7528\u7684\u97F3\u8272",
1826
+ args: {
1827
+ provider: tool.schema.string().optional().describe("TTS \u63D0\u4F9B\u5546\u540D\u79F0")
1828
+ },
1829
+ execute: async (args) => {
1830
+ const result = await handleToolCall("tts_list_voices", args);
1831
+ if (result.success === false) {
1832
+ throw new Error(result.error || "Unknown error");
1833
+ }
1834
+ return result;
1835
+ }
1836
+ });
1837
+ var ttsListProvidersTool = tool({
1838
+ description: "\u5217\u51FA\u6240\u6709\u5DF2\u6CE8\u518C\u7684 TTS \u63D0\u4F9B\u5546",
1839
+ args: {},
1840
+ execute: async () => {
1841
+ const result = await handleToolCall("tts_list_providers");
1842
+ if (result.success === false) {
1843
+ throw new Error(result.error || "Unknown error");
1844
+ }
1845
+ return result;
1846
+ }
1847
+ });
1848
+ var ttsStatusTool = tool({
1849
+ description: "\u83B7\u53D6\u5F53\u524D TTS \u64AD\u653E\u72B6\u6001",
1850
+ args: {},
1851
+ execute: async () => {
1852
+ const result = await handleToolCall("tts_status");
1853
+ if (result.success === false) {
1854
+ throw new Error(result.error || "Unknown error");
1855
+ }
1856
+ return result;
1857
+ }
1858
+ });
1859
+ var ttsStreamSpeakTool = tool({
1860
+ description: "\u542F\u52A8\u6D41\u5F0F\u6717\u8BFB\uFF08\u8C46\u5305\u6A21\u5F0F\uFF09\uFF0C\u8BA2\u9605AI\u56DE\u590D\u5E76\u8FB9\u751F\u6210\u8FB9\u6717\u8BFB",
1861
+ args: {
1862
+ text: tool.schema.string().optional().describe("\u521D\u59CB\u6587\u672C\uFF08\u53EF\u9009\uFF09"),
1863
+ voice: tool.schema.string().optional().describe("\u97F3\u8272ID"),
1864
+ model: tool.schema.enum(["sync", "async", "stream"]).optional().describe("\u5408\u6210\u6A21\u5F0F")
1865
+ },
1866
+ execute: async (args) => {
1867
+ const result = await handleToolCall("tts_stream_speak", args);
1868
+ if (result.success === false) {
1869
+ throw new Error(result.error || "Unknown error");
1870
+ }
1871
+ return result;
1872
+ }
1873
+ });
1874
+ var ttsStreamStopTool = tool({
1875
+ description: "\u505C\u6B62\u5F53\u524D\u6D41\u5F0F\u6717\u8BFB",
1876
+ args: {},
1877
+ execute: async () => {
1878
+ const result = await handleToolCall("tts_stream_stop");
1879
+ if (result.success === false) {
1880
+ throw new Error(result.error || "Unknown error");
1881
+ }
1882
+ return result;
1883
+ }
1884
+ });
1885
+ var ttsStreamStatusTool = tool({
1886
+ description: "\u83B7\u53D6\u5F53\u524D\u6D41\u5F0F\u6717\u8BFB\u72B6\u6001",
1887
+ args: {},
1888
+ execute: async () => {
1889
+ const result = await handleToolCall("tts_stream_status");
1890
+ if (result.success === false) {
1891
+ throw new Error(result.error || "Unknown error");
1892
+ }
1893
+ return typeof result === "string" ? result : JSON.stringify(result);
1894
+ }
1895
+ });
1896
+ var OcosayPlugin = async (_input, _options) => {
1897
+ console.info(`${pluginName}: initializing...`);
1898
+ const config = loadOrCreateConfig();
1899
+ await initialize({
1900
+ autoRead: config.autoRead,
1901
+ providers: {
1902
+ minimax: {
1903
+ apiKey: config.providers.minimax.apiKey,
1904
+ baseURL: config.providers.minimax.baseURL || void 0,
1905
+ voiceId: config.providers.minimax.voiceId || void 0
1906
+ }
1907
+ }
1908
+ });
1909
+ return {
1910
+ tool: {
1911
+ tts_speak: ttsSpeakTool,
1912
+ tts_stop: ttsStopTool,
1913
+ tts_pause: ttsPauseTool,
1914
+ tts_resume: ttsResumeTool,
1915
+ tts_list_voices: ttsListVoicesTool,
1916
+ tts_list_providers: ttsListProvidersTool,
1917
+ tts_status: ttsStatusTool,
1918
+ tts_stream_speak: ttsStreamSpeakTool,
1919
+ tts_stream_stop: ttsStreamStopTool,
1920
+ tts_stream_status: ttsStreamStatusTool
1921
+ },
1922
+ config: async () => {
1923
+ return;
1924
+ }
1925
+ };
1926
+ };
1927
+ var plugin_default = { server: OcosayPlugin };
1928
+ export {
1929
+ plugin_default as default
160
1930
  };
161
- export default { server: OcosayPlugin };
162
- //# sourceMappingURL=plugin.js.map
1931
+ //# sourceMappingURL=plugin.js.map