@mingxy/ocosay 1.1.33 → 1.2.0

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.
@@ -45,21 +45,6 @@ export var BackendType;
45
45
  BackendType["SPEAKER"] = "speaker";
46
46
  BackendType["AUTO"] = "auto";
47
47
  })(BackendType || (BackendType = {}));
48
- let naudiodonCache = null;
49
- async function tryLoadNaudiodon() {
50
- if (naudiodonCache !== null) {
51
- return naudiodonCache;
52
- }
53
- try {
54
- naudiodonCache = await import('naudiodon');
55
- return naudiodonCache;
56
- }
57
- catch (err) {
58
- logger.warn({ err }, 'failed to load naudiodon module');
59
- naudiodonCache = false;
60
- return null;
61
- }
62
- }
63
48
  function isNaudiodonAvailable() {
64
49
  try {
65
50
  require.resolve('naudiodon');
@@ -80,12 +65,27 @@ function isSpeakerAvailable() {
80
65
  return false;
81
66
  }
82
67
  }
68
+ /**
69
+ * 检测是否运行在 WSL (Windows Subsystem for Linux) 环境中
70
+ */
71
+ function isWSL() {
72
+ if (process.platform !== 'linux')
73
+ return false;
74
+ try {
75
+ const output = execSync('uname -r', { stdio: 'pipe', encoding: 'utf8' });
76
+ return output.toLowerCase().includes('microsoft');
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
83
82
  export function createBackend(type = BackendType.AUTO, options = {}) {
84
83
  const platform = process.platform;
85
84
  if (type !== BackendType.AUTO) {
86
85
  return createBackendByType(type, options);
87
86
  }
88
- if (isNaudiodonAvailable()) {
87
+ // WSL 环境下 naudiodon 可能无法工作(无法访问 Windows 音频设备),跳过
88
+ if (!isWSL() && isNaudiodonAvailable()) {
89
89
  try {
90
90
  const naudiodon = require('naudiodon');
91
91
  if (naudiodon) {
@@ -104,8 +104,29 @@ export function createBackend(type = BackendType.AUTO, options = {}) {
104
104
  switch (platform) {
105
105
  case 'darwin':
106
106
  return new AfplayBackend(options);
107
- case 'linux':
107
+ case 'linux': {
108
+ const wsl = isWSL();
109
+ if (wsl) {
110
+ logger.debug('Running on WSL, skipping naudiodon (may not work with Windows audio)');
111
+ }
108
112
  // Linux 环境检测顺序:naudiodon → aplay → play-sound → speaker → Howler
113
+ // WSL 环境下 naudiodon 可能无法工作,直接尝试其他后端
114
+ if (!wsl && isNaudiodonAvailable()) {
115
+ try {
116
+ const naudiodon = require('naudiodon');
117
+ if (naudiodon) {
118
+ const devices = naudiodon.getDevices();
119
+ if (devices && devices.length > 0) {
120
+ return new NaudiodonBackend(options);
121
+ }
122
+ logger.debug('naudiodon has no audio devices, skipping');
123
+ }
124
+ }
125
+ catch (err) {
126
+ logger.error({ err }, 'failed to initialize naudiodon backend');
127
+ }
128
+ }
129
+ // aplay 后端
109
130
  if (isCommandAvailable('aplay')) {
110
131
  const test = execCmd('aplay -l');
111
132
  if (test.success && !test.output.includes('no soundcards')) {
@@ -123,6 +144,7 @@ export function createBackend(type = BackendType.AUTO, options = {}) {
123
144
  // 彻底失败,使用 Howler 作为最后的回退
124
145
  logger.warn('All Linux audio backends failed, using HowlerBackend as fallback');
125
146
  return new HowlerBackend(options);
147
+ }
126
148
  case 'win32':
127
149
  return new PowerShellBackend(options);
128
150
  default:
@@ -7,6 +7,8 @@ import { execFile } from 'child_process';
7
7
  import { tmpdir } from 'os';
8
8
  import { join } from 'path';
9
9
  import { writeFileSync, unlinkSync, existsSync } from 'fs';
10
+ import { promisify } from 'util';
11
+ const execFileAsync = promisify(execFile);
10
12
  // 白名单:只允许特定路径格式(禁止 - 防止命令注入)
11
13
  const SAFE_PATH_REGEX = /^[\w\/\.]+$/;
12
14
  /**
@@ -45,59 +47,91 @@ export class PlaySoundBackend {
45
47
  // 等待 play-sound 播放完成
46
48
  await this.playWithPlaySound(filePath);
47
49
  }
48
- async playWithPlaySound(filePath) {
49
- try {
50
+ playWithPlaySound(filePath) {
51
+ return new Promise((resolve, reject) => {
50
52
  // 异步导入 play-sound
51
- const play = (await import('play-sound')).default;
52
- // 配置播放器选项
53
- const opts = {
54
- players: ['ffplay', 'aplay', 'mpg123', 'afplay'] // 优先级
55
- };
56
- // 对于 ffplay,使用无声模式
57
- if (process.platform === 'linux') {
58
- // ffplay 无声卡播放参数
59
- this.player = execFile('ffplay', [
60
- '-nodisp', // 不显示窗口
61
- '-autoexit', // 播放完自动退出
62
- '-loglevel', 'error', // 减少日志输出
63
- filePath
64
- ], (error) => {
65
- if (this._stopped)
66
- return;
67
- if (error) {
53
+ import('play-sound').then((playModule) => {
54
+ const play = playModule.default;
55
+ // 对于 ffplay,使用无声模式
56
+ if (process.platform === 'linux') {
57
+ // ffplay 无声卡播放参数
58
+ execFileAsync('ffplay', [
59
+ '-nodisp', // 不显示窗口
60
+ '-autoexit', // 播放完自动退出
61
+ '-loglevel', 'error', // 减少日志输出
62
+ filePath
63
+ ]).then(() => {
64
+ if (this._stopped) {
65
+ resolve();
66
+ return;
67
+ }
68
+ this._started = false;
69
+ this.events?.onEnd?.();
70
+ resolve();
71
+ }).catch((error) => {
72
+ if (this._stopped) {
73
+ resolve();
74
+ return;
75
+ }
76
+ this._started = false;
68
77
  this.handleError(error);
69
- return;
78
+ reject(error);
79
+ });
80
+ }
81
+ else {
82
+ // 使用 play-sound 的 Promise API
83
+ // play-sound 的类型定义不完善,需要用 any 访问 .play() 方法
84
+ const playerSound = play;
85
+ const p = playerSound.play(filePath);
86
+ if (p && p.then) {
87
+ // 返回 Promise 的情况
88
+ p.then(() => {
89
+ if (this._stopped) {
90
+ resolve();
91
+ return;
92
+ }
93
+ this._started = false;
94
+ this.events?.onEnd?.();
95
+ resolve();
96
+ }).catch((err) => {
97
+ if (this._stopped) {
98
+ resolve();
99
+ return;
100
+ }
101
+ this._started = false;
102
+ this.handleError(err);
103
+ reject(err);
104
+ });
70
105
  }
71
- this._started = false;
72
- this.events?.onEnd?.();
73
- });
74
- }
75
- else {
76
- // 使用 play-sound 的默认行为
77
- const audio = play;
78
- const p = audio.play(filePath, (err) => {
79
- if (this._stopped)
80
- return;
81
- if (err) {
82
- this.handleError(err);
83
- return;
106
+ else if (p && p.kill) {
107
+ // 返回 ChildProcess 的情况
108
+ this.player = p;
109
+ p.on('error', (error) => {
110
+ this.handleError(error);
111
+ reject(error);
112
+ });
113
+ p.on('close', () => {
114
+ if (this._stopped) {
115
+ resolve();
116
+ return;
117
+ }
118
+ this._started = false;
119
+ this.events?.onEnd?.();
120
+ resolve();
121
+ });
122
+ }
123
+ else {
124
+ // 无法识别的返回类型,直接 resolve
125
+ resolve();
84
126
  }
85
- this._started = false;
86
- this.events?.onEnd?.();
87
- });
88
- if (p && p.kill) {
89
- this.player = p;
90
127
  }
91
- }
92
- if (this.player) {
93
- this.player.on('error', (error) => {
94
- this.handleError(error);
95
- });
96
- }
97
- }
98
- catch (err) {
99
- this.handleError(err instanceof Error ? err : new Error(String(err)));
100
- }
128
+ }).catch((err) => {
129
+ const error = err instanceof Error ? err : new Error(String(err));
130
+ error.message = `play-sound load failed: ${error.message}`;
131
+ this.handleError(error);
132
+ reject(error);
133
+ });
134
+ });
101
135
  }
102
136
  write(chunk) {
103
137
  if (this._stopped)
@@ -92,7 +92,8 @@ export class SpeakerBackend {
92
92
  }
93
93
  stripWavHeader(chunk) {
94
94
  // 跳过 44 字节的 WAV 头
95
- return chunk.slice(44);
95
+ // 使用 subarray 替代 deprecated 的 slice
96
+ return chunk.subarray(44);
96
97
  }
97
98
  createSpeaker() {
98
99
  try {
@@ -1,6 +1,6 @@
1
1
  export type ToastVariant = 'success' | 'error' | 'info' | 'warning';
2
2
  export interface ToastOptions {
3
- title: string;
3
+ title?: string;
4
4
  message: string;
5
5
  variant?: ToastVariant;
6
6
  duration?: number;
@@ -17,10 +17,16 @@ declare class NotificationService {
17
17
  showToast(options: ToastOptions): boolean;
18
18
  private scheduleRetry;
19
19
  private flushPending;
20
+ success(message: string, duration?: number): boolean;
20
21
  success(title: string, message: string, duration?: number): boolean;
22
+ error(message: string, duration?: number): boolean;
21
23
  error(title: string, message: string, duration?: number): boolean;
24
+ info(message: string, duration?: number): boolean;
22
25
  info(title: string, message: string, duration?: number): boolean;
26
+ warning(message: string, duration?: number): boolean;
23
27
  warning(title: string, message: string, duration?: number): boolean;
28
+ showSpinnerToast(title: string, message: string, duration?: number): Promise<void>;
29
+ private getTitleForVariant;
24
30
  }
25
31
  export declare const notificationService: NotificationService;
26
32
  export default notificationService;
@@ -1,6 +1,7 @@
1
1
  // src/core/notification.ts
2
2
  import { createModuleLogger } from '../utils/logger';
3
3
  const logger = createModuleLogger('NotificationService');
4
+ const SISYPHUS_SPINNER = ['·', '•', '●', '○', '◌', '◦', ' '];
4
5
  /**
5
6
  * NotificationService - 统一 Toast 通知管理
6
7
  * 参照 DCP 实现,不做防御性检查,直接调用并用 try-catch 处理
@@ -15,10 +16,11 @@ class NotificationService {
15
16
  this.flushPending();
16
17
  }
17
18
  showToast(options) {
18
- const { title, message, variant = 'info', duration = 5000 } = options;
19
+ const title = options.title || this.getTitleForVariant(options.variant || 'info');
20
+ const { message, variant = 'info', duration = 5000 } = options;
19
21
  if (!this.tui) {
20
22
  logger.debug({ title }, 'tui not ready, queueing toast');
21
- this.pendingToasts.push(options);
23
+ this.pendingToasts.push({ ...options, title });
22
24
  this.scheduleRetry();
23
25
  return false;
24
26
  }
@@ -38,7 +40,7 @@ class NotificationService {
38
40
  catch (err) {
39
41
  // 参照 DCP:捕获异常但不抛出
40
42
  logger.warn({ err, title }, 'toast call failed, queueing for retry');
41
- this.pendingToasts.push(options);
43
+ this.pendingToasts.push({ ...options, title });
42
44
  this.scheduleRetry();
43
45
  return false;
44
46
  }
@@ -68,17 +70,68 @@ class NotificationService {
68
70
  }
69
71
  }
70
72
  }
71
- success(title, message, duration) {
72
- return this.showToast({ title, message, variant: 'success', duration });
73
+ success(titleOrMessage, messageOrDuration, duration) {
74
+ if (typeof messageOrDuration === 'string') {
75
+ // 3参数: success(title, message, duration)
76
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'success', duration });
77
+ }
78
+ // 2参数: success(message, duration) 或 success(message)
79
+ return this.showToast({ message: titleOrMessage, variant: 'success', duration: messageOrDuration });
73
80
  }
74
- error(title, message, duration) {
75
- return this.showToast({ title, message, variant: 'error', duration });
81
+ error(titleOrMessage, messageOrDuration, duration) {
82
+ if (typeof messageOrDuration === 'string') {
83
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'error', duration });
84
+ }
85
+ return this.showToast({ message: titleOrMessage, variant: 'error', duration: messageOrDuration || 8000 });
76
86
  }
77
- info(title, message, duration) {
78
- return this.showToast({ title, message, variant: 'info', duration });
87
+ info(titleOrMessage, messageOrDuration, duration) {
88
+ if (typeof messageOrDuration === 'string') {
89
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'info', duration });
90
+ }
91
+ return this.showToast({ message: titleOrMessage, variant: 'info', duration: messageOrDuration });
92
+ }
93
+ warning(titleOrMessage, messageOrDuration, duration) {
94
+ if (typeof messageOrDuration === 'string') {
95
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: 'warning', duration });
96
+ }
97
+ return this.showToast({ message: titleOrMessage, variant: 'warning', duration: messageOrDuration });
98
+ }
99
+ async showSpinnerToast(title, message, duration = 2000) {
100
+ const frameInterval = 100;
101
+ const totalFrames = Math.ceil(duration / frameInterval);
102
+ for (let i = 0; i < totalFrames; i++) {
103
+ const spinner = SISYPHUS_SPINNER[i % SISYPHUS_SPINNER.length];
104
+ const toastDuration = Math.min(frameInterval + 50, duration - i * frameInterval);
105
+ if (toastDuration <= 0)
106
+ break;
107
+ if (this.tui?.showToast) {
108
+ try {
109
+ await this.tui.showToast({
110
+ body: {
111
+ title: `${spinner} ${title}`,
112
+ message,
113
+ variant: 'info',
114
+ duration: toastDuration
115
+ },
116
+ });
117
+ }
118
+ catch (err) {
119
+ logger.warn({ err }, 'showSpinnerToast failed');
120
+ }
121
+ }
122
+ if (i < totalFrames - 1) {
123
+ await new Promise((resolve) => setTimeout(resolve, frameInterval));
124
+ }
125
+ }
79
126
  }
80
- warning(title, message, duration) {
81
- return this.showToast({ title, message, variant: 'warning', duration });
127
+ getTitleForVariant(variant) {
128
+ const titles = {
129
+ success: 'Success',
130
+ error: 'Error',
131
+ warning: 'Warning',
132
+ info: 'Info'
133
+ };
134
+ return titles[variant];
82
135
  }
83
136
  }
84
137
  export const notificationService = new NotificationService();
@@ -121,7 +121,7 @@ export class AudioPlayer extends EventEmitter {
121
121
  * 使用 AudioBackend 统一后端播放
122
122
  */
123
123
  async playFile(filePath, _format) {
124
- await this.backend.start(filePath);
124
+ await Promise.resolve(this.backend.start(filePath));
125
125
  }
126
126
  pause() {
127
127
  if (!this._playing || this._paused)
@@ -38,6 +38,7 @@ export declare class StreamPlayer extends EventEmitter {
38
38
  private _started;
39
39
  private _paused;
40
40
  private _stopped;
41
+ private _starting;
41
42
  private format;
42
43
  private events?;
43
44
  constructor(options?: StreamPlayerOptions);
@@ -45,12 +46,12 @@ export declare class StreamPlayer extends EventEmitter {
45
46
  * 开始播放
46
47
  * 初始化后端,准备接收音频数据
47
48
  */
48
- start(): void;
49
+ start(): Promise<void>;
49
50
  /**
50
51
  * 写入音频数据块(边收边播)
51
52
  * 如果尚未 start(),会自动调用
52
53
  */
53
- write(chunk: Buffer): void;
54
+ write(chunk: Buffer): Promise<void>;
54
55
  /**
55
56
  * 结束写入
56
57
  * 通知后端写入完成,但保持播放直到结束
@@ -18,14 +18,15 @@ export class StreamPlayer extends EventEmitter {
18
18
  _started = false;
19
19
  _paused = false;
20
20
  _stopped = false;
21
+ _starting = false;
21
22
  format = 'mp3';
22
23
  events;
23
24
  constructor(options = {}) {
24
25
  super();
25
26
  this.format = options.format || 'mp3';
26
27
  this.events = options.events;
27
- // 创建音频后端
28
- const backendType = options.backendType || BackendType.NAUDIODON;
28
+ // 创建音频后端,默认使用 AUTO 自动选择合适的后端
29
+ const backendType = options.backendType || BackendType.AUTO;
29
30
  this.backend = createBackend(backendType, {
30
31
  format: this.format,
31
32
  events: {
@@ -65,7 +66,7 @@ export class StreamPlayer extends EventEmitter {
65
66
  * 开始播放
66
67
  * 初始化后端,准备接收音频数据
67
68
  */
68
- start() {
69
+ async start() {
69
70
  if (this._started) {
70
71
  return;
71
72
  }
@@ -73,8 +74,8 @@ export class StreamPlayer extends EventEmitter {
73
74
  this.handleError(new Error('Audio backend not initialized'));
74
75
  return;
75
76
  }
76
- // 初始化后端
77
- this.backend.start('');
77
+ // 初始化后端(playsound-backend 是异步的,需 await)
78
+ await Promise.resolve(this.backend.start(''));
78
79
  this._started = true;
79
80
  this._stopped = false;
80
81
  this._paused = false;
@@ -84,18 +85,29 @@ export class StreamPlayer extends EventEmitter {
84
85
  * 写入音频数据块(边收边播)
85
86
  * 如果尚未 start(),会自动调用
86
87
  */
87
- write(chunk) {
88
+ async write(chunk) {
88
89
  // 如果已停止,直接忽略
89
90
  if (this._stopped) {
90
91
  return;
91
92
  }
92
- // 如果未启动,自动启动
93
+ // 如果未启动,防止竞态条件并自动启动
93
94
  if (!this._started) {
94
- this.start();
95
+ // 防止并发启动
96
+ while (this._starting) {
97
+ await new Promise(resolve => setTimeout(resolve, 10));
98
+ }
99
+ this._starting = true;
100
+ try {
101
+ await this.start();
102
+ }
103
+ finally {
104
+ this._starting = false;
105
+ }
95
106
  }
96
107
  // 写入数据到后端
97
108
  if (this.backend) {
98
- this.backend.write(chunk);
109
+ // backend.write() 可能返回 Promise 或 void,用 Promise.resolve 处理
110
+ await Promise.resolve(this.backend.write(chunk));
99
111
  this._bytesWritten += chunk.length;
100
112
  this.events?.onProgress?.(this._bytesWritten);
101
113
  this.emit('progress', this._bytesWritten);
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ function initializeStreamComponents(config) {
65
65
  const playerEvents = {
66
66
  onStart: () => { },
67
67
  onEnd: () => { },
68
- onProgress: (bytesWritten) => { },
68
+ onProgress: (_bytesWritten) => { },
69
69
  onError: (error) => logger.error({ error }, 'stream player error'),
70
70
  onStop: () => { }
71
71
  };
@@ -161,7 +161,7 @@ export async function destroy() {
161
161
  streamingSynthesizer = undefined;
162
162
  }
163
163
  if (streamPlayer) {
164
- await streamPlayer.stop();
164
+ streamPlayer.stop();
165
165
  streamPlayer = undefined;
166
166
  }
167
167
  if (speaker) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/ocosay",
3
- "version": "1.1.32",
3
+ "version": "1.2.0",
4
4
  "description": "OpenCode TTS 播放插件 - 支持豆包模式边接收边朗读",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",
package/dist/plugin.js CHANGED
@@ -7471,6 +7471,8 @@ import { execFile as execFile3 } from "child_process";
7471
7471
  import { tmpdir as tmpdir4 } from "os";
7472
7472
  import { join as join5 } from "path";
7473
7473
  import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync4, existsSync as existsSync5 } from "fs";
7474
+ import { promisify } from "util";
7475
+ var execFileAsync = promisify(execFile3);
7474
7476
  var SAFE_PATH_REGEX4 = /^[\w\/\.]+$/;
7475
7477
  var PlaySoundBackend = class {
7476
7478
  name = "play-sound";
@@ -7498,55 +7500,84 @@ var PlaySoundBackend = class {
7498
7500
  this.events?.onStart?.();
7499
7501
  await this.playWithPlaySound(filePath);
7500
7502
  }
7501
- async playWithPlaySound(filePath) {
7502
- try {
7503
- const play = (await import("play-sound")).default;
7504
- const opts = {
7505
- players: ["ffplay", "aplay", "mpg123", "afplay"]
7506
- // 优先级
7507
- };
7508
- if (process.platform === "linux") {
7509
- this.player = execFile3("ffplay", [
7510
- "-nodisp",
7511
- // 不显示窗口
7512
- "-autoexit",
7513
- // 播放完自动退出
7514
- "-loglevel",
7515
- "error",
7516
- // 减少日志输出
7517
- filePath
7518
- ], (error) => {
7519
- if (this._stopped) return;
7520
- if (error) {
7503
+ playWithPlaySound(filePath) {
7504
+ return new Promise((resolve, reject) => {
7505
+ import("play-sound").then((playModule) => {
7506
+ const play = playModule.default;
7507
+ if (process.platform === "linux") {
7508
+ execFileAsync("ffplay", [
7509
+ "-nodisp",
7510
+ // 不显示窗口
7511
+ "-autoexit",
7512
+ // 播放完自动退出
7513
+ "-loglevel",
7514
+ "error",
7515
+ // 减少日志输出
7516
+ filePath
7517
+ ]).then(() => {
7518
+ if (this._stopped) {
7519
+ resolve();
7520
+ return;
7521
+ }
7522
+ this._started = false;
7523
+ this.events?.onEnd?.();
7524
+ resolve();
7525
+ }).catch((error) => {
7526
+ if (this._stopped) {
7527
+ resolve();
7528
+ return;
7529
+ }
7530
+ this._started = false;
7521
7531
  this.handleError(error);
7522
- return;
7523
- }
7524
- this._started = false;
7525
- this.events?.onEnd?.();
7526
- });
7527
- } else {
7528
- const audio = play;
7529
- const p = audio.play(filePath, (err) => {
7530
- if (this._stopped) return;
7531
- if (err) {
7532
- this.handleError(err);
7533
- return;
7532
+ reject(error);
7533
+ });
7534
+ } else {
7535
+ const playerSound = play;
7536
+ const p = playerSound.play(filePath);
7537
+ if (p && p.then) {
7538
+ p.then(() => {
7539
+ if (this._stopped) {
7540
+ resolve();
7541
+ return;
7542
+ }
7543
+ this._started = false;
7544
+ this.events?.onEnd?.();
7545
+ resolve();
7546
+ }).catch((err) => {
7547
+ if (this._stopped) {
7548
+ resolve();
7549
+ return;
7550
+ }
7551
+ this._started = false;
7552
+ this.handleError(err);
7553
+ reject(err);
7554
+ });
7555
+ } else if (p && p.kill) {
7556
+ this.player = p;
7557
+ p.on("error", (error) => {
7558
+ this.handleError(error);
7559
+ reject(error);
7560
+ });
7561
+ p.on("close", () => {
7562
+ if (this._stopped) {
7563
+ resolve();
7564
+ return;
7565
+ }
7566
+ this._started = false;
7567
+ this.events?.onEnd?.();
7568
+ resolve();
7569
+ });
7570
+ } else {
7571
+ resolve();
7534
7572
  }
7535
- this._started = false;
7536
- this.events?.onEnd?.();
7537
- });
7538
- if (p && p.kill) {
7539
- this.player = p;
7540
7573
  }
7541
- }
7542
- if (this.player) {
7543
- this.player.on("error", (error) => {
7544
- this.handleError(error);
7545
- });
7546
- }
7547
- } catch (err) {
7548
- this.handleError(err instanceof Error ? err : new Error(String(err)));
7549
- }
7574
+ }).catch((err) => {
7575
+ const error = err instanceof Error ? err : new Error(String(err));
7576
+ error.message = `play-sound load failed: ${error.message}`;
7577
+ this.handleError(error);
7578
+ reject(error);
7579
+ });
7580
+ });
7550
7581
  }
7551
7582
  write(chunk) {
7552
7583
  if (this._stopped) return;
@@ -7694,7 +7725,7 @@ var SpeakerBackend = class {
7694
7725
  return false;
7695
7726
  }
7696
7727
  stripWavHeader(chunk) {
7697
- return chunk.slice(44);
7728
+ return chunk.subarray(44);
7698
7729
  }
7699
7730
  createSpeaker() {
7700
7731
  try {
@@ -7770,6 +7801,7 @@ import { execSync } from "child_process";
7770
7801
 
7771
7802
  // src/core/notification.ts
7772
7803
  var logger3 = createModuleLogger("NotificationService");
7804
+ var SISYPHUS_SPINNER = ["\xB7", "\u2022", "\u25CF", "\u25CB", "\u25CC", "\u25E6", " "];
7773
7805
  var NotificationService = class {
7774
7806
  tui = null;
7775
7807
  pendingToasts = [];
@@ -7780,10 +7812,11 @@ var NotificationService = class {
7780
7812
  this.flushPending();
7781
7813
  }
7782
7814
  showToast(options) {
7783
- const { title, message, variant = "info", duration = 5e3 } = options;
7815
+ const title = options.title || this.getTitleForVariant(options.variant || "info");
7816
+ const { message, variant = "info", duration = 5e3 } = options;
7784
7817
  if (!this.tui) {
7785
7818
  logger3.debug({ title }, "tui not ready, queueing toast");
7786
- this.pendingToasts.push(options);
7819
+ this.pendingToasts.push({ ...options, title });
7787
7820
  this.scheduleRetry();
7788
7821
  return false;
7789
7822
  }
@@ -7800,7 +7833,7 @@ var NotificationService = class {
7800
7833
  return true;
7801
7834
  } catch (err) {
7802
7835
  logger3.warn({ err, title }, "toast call failed, queueing for retry");
7803
- this.pendingToasts.push(options);
7836
+ this.pendingToasts.push({ ...options, title });
7804
7837
  this.scheduleRetry();
7805
7838
  return false;
7806
7839
  }
@@ -7826,17 +7859,64 @@ var NotificationService = class {
7826
7859
  }
7827
7860
  }
7828
7861
  }
7829
- success(title, message, duration) {
7830
- return this.showToast({ title, message, variant: "success", duration });
7862
+ success(titleOrMessage, messageOrDuration, duration) {
7863
+ if (typeof messageOrDuration === "string") {
7864
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: "success", duration });
7865
+ }
7866
+ return this.showToast({ message: titleOrMessage, variant: "success", duration: messageOrDuration });
7867
+ }
7868
+ error(titleOrMessage, messageOrDuration, duration) {
7869
+ if (typeof messageOrDuration === "string") {
7870
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: "error", duration });
7871
+ }
7872
+ return this.showToast({ message: titleOrMessage, variant: "error", duration: messageOrDuration || 8e3 });
7873
+ }
7874
+ info(titleOrMessage, messageOrDuration, duration) {
7875
+ if (typeof messageOrDuration === "string") {
7876
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: "info", duration });
7877
+ }
7878
+ return this.showToast({ message: titleOrMessage, variant: "info", duration: messageOrDuration });
7831
7879
  }
7832
- error(title, message, duration) {
7833
- return this.showToast({ title, message, variant: "error", duration });
7880
+ warning(titleOrMessage, messageOrDuration, duration) {
7881
+ if (typeof messageOrDuration === "string") {
7882
+ return this.showToast({ title: titleOrMessage, message: messageOrDuration, variant: "warning", duration });
7883
+ }
7884
+ return this.showToast({ message: titleOrMessage, variant: "warning", duration: messageOrDuration });
7834
7885
  }
7835
- info(title, message, duration) {
7836
- return this.showToast({ title, message, variant: "info", duration });
7886
+ async showSpinnerToast(title, message, duration = 2e3) {
7887
+ const frameInterval = 100;
7888
+ const totalFrames = Math.ceil(duration / frameInterval);
7889
+ for (let i = 0; i < totalFrames; i++) {
7890
+ const spinner = SISYPHUS_SPINNER[i % SISYPHUS_SPINNER.length];
7891
+ const toastDuration = Math.min(frameInterval + 50, duration - i * frameInterval);
7892
+ if (toastDuration <= 0) break;
7893
+ if (this.tui?.showToast) {
7894
+ try {
7895
+ await this.tui.showToast({
7896
+ body: {
7897
+ title: `${spinner} ${title}`,
7898
+ message,
7899
+ variant: "info",
7900
+ duration: toastDuration
7901
+ }
7902
+ });
7903
+ } catch (err) {
7904
+ logger3.warn({ err }, "showSpinnerToast failed");
7905
+ }
7906
+ }
7907
+ if (i < totalFrames - 1) {
7908
+ await new Promise((resolve) => setTimeout(resolve, frameInterval));
7909
+ }
7910
+ }
7837
7911
  }
7838
- warning(title, message, duration) {
7839
- return this.showToast({ title, message, variant: "warning", duration });
7912
+ getTitleForVariant(variant) {
7913
+ const titles = {
7914
+ success: "Success",
7915
+ error: "Error",
7916
+ warning: "Warning",
7917
+ info: "Info"
7918
+ };
7919
+ return titles[variant];
7840
7920
  }
7841
7921
  };
7842
7922
  var notificationService = new NotificationService();
@@ -7871,12 +7951,21 @@ function isSpeakerAvailable() {
7871
7951
  return false;
7872
7952
  }
7873
7953
  }
7954
+ function isWSL() {
7955
+ if (process.platform !== "linux") return false;
7956
+ try {
7957
+ const output = execSync("uname -r", { stdio: "pipe", encoding: "utf8" });
7958
+ return output.toLowerCase().includes("microsoft");
7959
+ } catch {
7960
+ return false;
7961
+ }
7962
+ }
7874
7963
  function createBackend(type = "auto" /* AUTO */, options = {}) {
7875
7964
  const platform = process.platform;
7876
7965
  if (type !== "auto" /* AUTO */) {
7877
7966
  return createBackendByType(type, options);
7878
7967
  }
7879
- if (isNaudiodonAvailable()) {
7968
+ if (!isWSL() && isNaudiodonAvailable()) {
7880
7969
  try {
7881
7970
  const naudiodon = __require("naudiodon");
7882
7971
  if (naudiodon) {
@@ -7898,7 +7987,25 @@ function createBackend(type = "auto" /* AUTO */, options = {}) {
7898
7987
  switch (platform) {
7899
7988
  case "darwin":
7900
7989
  return new AfplayBackend(options);
7901
- case "linux":
7990
+ case "linux": {
7991
+ const wsl = isWSL();
7992
+ if (wsl) {
7993
+ logger.debug("Running on WSL, skipping naudiodon (may not work with Windows audio)");
7994
+ }
7995
+ if (!wsl && isNaudiodonAvailable()) {
7996
+ try {
7997
+ const naudiodon = __require("naudiodon");
7998
+ if (naudiodon) {
7999
+ const devices = naudiodon.getDevices();
8000
+ if (devices && devices.length > 0) {
8001
+ return new NaudiodonBackend(options);
8002
+ }
8003
+ logger.debug("naudiodon has no audio devices, skipping");
8004
+ }
8005
+ } catch (err) {
8006
+ logger.error({ err }, "failed to initialize naudiodon backend");
8007
+ }
8008
+ }
7902
8009
  if (isCommandAvailable("aplay")) {
7903
8010
  const test = execCmd("aplay -l");
7904
8011
  if (test.success && !test.output.includes("no soundcards")) {
@@ -7913,6 +8020,7 @@ function createBackend(type = "auto" /* AUTO */, options = {}) {
7913
8020
  }
7914
8021
  logger.warn("All Linux audio backends failed, using HowlerBackend as fallback");
7915
8022
  return new HowlerBackend(options);
8023
+ }
7916
8024
  case "win32":
7917
8025
  return new PowerShellBackend(options);
7918
8026
  default:
@@ -8047,7 +8155,7 @@ var AudioPlayer = class extends EventEmitter {
8047
8155
  * 使用 AudioBackend 统一后端播放
8048
8156
  */
8049
8157
  async playFile(filePath, _format) {
8050
- await this.backend.start(filePath);
8158
+ await Promise.resolve(this.backend.start(filePath));
8051
8159
  }
8052
8160
  pause() {
8053
8161
  if (!this._playing || this._paused) return;
@@ -8298,12 +8406,10 @@ async function listVoices(providerName) {
8298
8406
 
8299
8407
  // src/services/speaker-service.ts
8300
8408
  var SpeakerService = class {
8301
- constructor(options = {}) {
8302
- this.options = options;
8409
+ speaker;
8410
+ constructor(_options = {}) {
8303
8411
  this.speaker = getDefaultSpeaker();
8304
8412
  }
8305
- options;
8306
- speaker;
8307
8413
  async speak(text, options) {
8308
8414
  const timestamp = this.getTimestamp();
8309
8415
  logger.info(`[Ocosay][${timestamp}][INFO][Speaker] \u5BF9\u5E94\u4E8B\u4EF6{\u64AD\u653E\u5F00\u59CB} - \u6587\u672C\u957F\u5EA6: ${text.length}`);
@@ -8379,13 +8485,14 @@ var StreamPlayer = class extends EventEmitter3 {
8379
8485
  _started = false;
8380
8486
  _paused = false;
8381
8487
  _stopped = false;
8488
+ _starting = false;
8382
8489
  format = "mp3";
8383
8490
  events;
8384
8491
  constructor(options = {}) {
8385
8492
  super();
8386
8493
  this.format = options.format || "mp3";
8387
8494
  this.events = options.events;
8388
- const backendType = options.backendType || "naudiodon" /* NAUDIODON */;
8495
+ const backendType = options.backendType || "auto" /* AUTO */;
8389
8496
  this.backend = createBackend(backendType, {
8390
8497
  format: this.format,
8391
8498
  events: {
@@ -8425,7 +8532,7 @@ var StreamPlayer = class extends EventEmitter3 {
8425
8532
  * 开始播放
8426
8533
  * 初始化后端,准备接收音频数据
8427
8534
  */
8428
- start() {
8535
+ async start() {
8429
8536
  if (this._started) {
8430
8537
  return;
8431
8538
  }
@@ -8433,7 +8540,7 @@ var StreamPlayer = class extends EventEmitter3 {
8433
8540
  this.handleError(new Error("Audio backend not initialized"));
8434
8541
  return;
8435
8542
  }
8436
- this.backend.start("");
8543
+ await Promise.resolve(this.backend.start(""));
8437
8544
  this._started = true;
8438
8545
  this._stopped = false;
8439
8546
  this._paused = false;
@@ -8443,15 +8550,23 @@ var StreamPlayer = class extends EventEmitter3 {
8443
8550
  * 写入音频数据块(边收边播)
8444
8551
  * 如果尚未 start(),会自动调用
8445
8552
  */
8446
- write(chunk) {
8553
+ async write(chunk) {
8447
8554
  if (this._stopped) {
8448
8555
  return;
8449
8556
  }
8450
8557
  if (!this._started) {
8451
- this.start();
8558
+ while (this._starting) {
8559
+ await new Promise((resolve) => setTimeout(resolve, 10));
8560
+ }
8561
+ this._starting = true;
8562
+ try {
8563
+ await this.start();
8564
+ } finally {
8565
+ this._starting = false;
8566
+ }
8452
8567
  }
8453
8568
  if (this.backend) {
8454
- this.backend.write(chunk);
8569
+ await Promise.resolve(this.backend.write(chunk));
8455
8570
  this._bytesWritten += chunk.length;
8456
8571
  this.events?.onProgress?.(this._bytesWritten);
8457
8572
  this.emit("progress", this._bytesWritten);
@@ -8641,7 +8756,7 @@ var StreamingService = class extends EventEmitter4 {
8641
8756
  );
8642
8757
  }
8643
8758
  const player = this.initPlayer();
8644
- player.start();
8759
+ await player.start();
8645
8760
  this._isActive = true;
8646
8761
  this._bytesWritten = 0;
8647
8762
  const result = await provider.speak(text, {
@@ -8667,7 +8782,7 @@ var StreamingService = class extends EventEmitter4 {
8667
8782
  if (result.isStream && result.audioData instanceof ReadableStream) {
8668
8783
  await this.streamAudioChunks(result.audioData, player);
8669
8784
  } else if (Buffer.isBuffer(result.audioData)) {
8670
- player.write(result.audioData);
8785
+ await player.write(result.audioData);
8671
8786
  player.end();
8672
8787
  }
8673
8788
  }
@@ -8684,7 +8799,7 @@ var StreamingService = class extends EventEmitter4 {
8684
8799
  }
8685
8800
  if (value) {
8686
8801
  const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
8687
- player.write(chunk);
8802
+ await player.write(chunk);
8688
8803
  }
8689
8804
  }
8690
8805
  } finally {
@@ -8946,7 +9061,6 @@ var MiniMaxProvider = class extends BaseTTSProvider {
8946
9061
  config;
8947
9062
  httpClient;
8948
9063
  wsConnection;
8949
- currentAudioData = [];
8950
9064
  audioFormat = "mp3";
8951
9065
  constructor(config) {
8952
9066
  super();
@@ -9672,7 +9786,7 @@ function initializeStreamComponents(config) {
9672
9786
  },
9673
9787
  onEnd: () => {
9674
9788
  },
9675
- onProgress: (bytesWritten) => {
9789
+ onProgress: (_bytesWritten) => {
9676
9790
  },
9677
9791
  onError: (error) => logger.error({ error }, "stream player error"),
9678
9792
  onStop: () => {
@@ -10123,9 +10237,8 @@ async function ensureSpeakerInstalledAsync() {
10123
10237
  await ensurePlaySoundInstalled();
10124
10238
  }
10125
10239
  async function initAsync() {
10126
- setTimeout(async () => {
10127
- await ensureSpeakerInstalledAsync();
10128
- }, 100);
10240
+ await new Promise((resolve) => setTimeout(resolve, 100));
10241
+ await ensureSpeakerInstalledAsync();
10129
10242
  }
10130
10243
  async function ensurePlaySoundInstalled() {
10131
10244
  const dep = "play-sound";
@@ -10434,6 +10547,45 @@ var server = (async (input, _options) => {
10434
10547
  global.__opencode_tui__ = opencodeTui;
10435
10548
  notificationService.setTui(opencodeTui);
10436
10549
  const config = loadOrCreateConfig();
10550
+ function collectEnvironmentInfo() {
10551
+ const lines = [];
10552
+ lines.push(`Auto-read: ${config.autoRead ? "ON" : "OFF"}`);
10553
+ let naudiodonOk = false;
10554
+ try {
10555
+ pluginRequire.resolve("naudiodon");
10556
+ naudiodonOk = true;
10557
+ } catch {
10558
+ naudiodonOk = false;
10559
+ }
10560
+ const playSoundOk = isModuleInstalled("play-sound");
10561
+ const platform = process.platform;
10562
+ let backend = "unknown";
10563
+ if (naudiodonOk) {
10564
+ backend = "naudiodon";
10565
+ } else if (platform === "linux") {
10566
+ if (checkAlsa()) {
10567
+ backend = "aplay";
10568
+ } else if (checkFFplay()) {
10569
+ backend = "ffplay";
10570
+ } else {
10571
+ backend = "howler";
10572
+ }
10573
+ } else if (platform === "darwin") {
10574
+ backend = "afplay";
10575
+ } else if (platform === "win32") {
10576
+ backend = "powershell";
10577
+ } else {
10578
+ backend = "howler";
10579
+ }
10580
+ const ffplayOk = checkFFplay();
10581
+ const alsaOk = checkAlsa();
10582
+ lines.push(`naudiodon: ${naudiodonOk ? "\u2713" : "\u2717"}`);
10583
+ lines.push(`play-sound: ${playSoundOk ? "\u2713" : "\u2717"}`);
10584
+ lines.push(`backend: ${backend}`);
10585
+ lines.push(`ffplay: ${ffplayOk ? "\u2713" : "\u2717"}`);
10586
+ lines.push(`alsa: ${alsaOk ? "\u2713" : "\u2717"}`);
10587
+ return lines.join("\n");
10588
+ }
10437
10589
  try {
10438
10590
  await initialize({
10439
10591
  autoRead: config.autoRead,
@@ -10449,21 +10601,22 @@ var server = (async (input, _options) => {
10449
10601
  initError = err instanceof Error ? err : new Error(String(err));
10450
10602
  logger8.error({ error: initError }, "initialization failed");
10451
10603
  }
10452
- initAsync();
10604
+ await initAsync();
10453
10605
  await ensureNaudiodonCompiled();
10454
10606
  await ensureOptionalDepsInstalled();
10455
10607
  await checkAudioEnvironmentForBackend();
10456
10608
  setTimeout(() => {
10609
+ const envInfo = collectEnvironmentInfo();
10457
10610
  if (initError) {
10458
10611
  notificationService.error(
10459
- `Ocosay v${pluginVersion} Init Failed`,
10460
- "Please check your config file",
10612
+ `Ocosay v${pluginVersion} Init Failed
10613
+ Please check your config file`,
10461
10614
  8e3
10462
10615
  );
10463
10616
  } else {
10464
- notificationService.success(
10617
+ notificationService.showSpinnerToast(
10465
10618
  `Ocosay v${pluginVersion} Ready`,
10466
- `Auto-read: ${config.autoRead ? "ON" : "OFF"}`,
10619
+ envInfo,
10467
10620
  5e3
10468
10621
  );
10469
10622
  }
@@ -35,7 +35,6 @@ export declare class MiniMaxProvider extends BaseTTSProvider {
35
35
  private config;
36
36
  private httpClient;
37
37
  private wsConnection?;
38
- private currentAudioData;
39
38
  private audioFormat;
40
39
  constructor(config: MiniMaxConfig);
41
40
  initialize(): Promise<void>;
@@ -24,7 +24,6 @@ export class MiniMaxProvider extends BaseTTSProvider {
24
24
  config;
25
25
  httpClient;
26
26
  wsConnection;
27
- currentAudioData = [];
28
27
  audioFormat = 'mp3';
29
28
  constructor(config) {
30
29
  super();
@@ -1,17 +1,26 @@
1
1
  export type ToastType = 'success' | 'error' | 'warning' | 'info';
2
+ export interface ShowToastOptions {
3
+ title?: string;
4
+ message: string;
5
+ variant?: ToastType;
6
+ duration?: number;
7
+ }
2
8
  export declare class NotificationService {
3
9
  private constructor();
4
10
  static getInstance(): NotificationService;
5
11
  initialize(tuiInstance: any): void;
6
12
  isReady(): boolean;
13
+ showToast(options: ShowToastOptions): void;
7
14
  showToast(message: string, type?: ToastType): void;
8
- success(message: string): void;
9
- error(message: string): void;
10
- warning(message: string): void;
11
- info(message: string): void;
15
+ success(message: string, duration?: number): void;
16
+ error(message: string, duration?: number): void;
17
+ warning(message: string, duration?: number): void;
18
+ info(message: string, duration?: number): void;
19
+ showSpinnerToast(title: string, message: string, duration?: number): Promise<void>;
12
20
  private getTitleForType;
13
21
  private fallbackLog;
14
22
  }
23
+ export declare function showToast(options: ShowToastOptions): void;
15
24
  export declare function showToast(message: string, type?: ToastType): void;
16
25
  export declare function initializeNotificationService(tuiInstance: any): void;
17
26
  //# sourceMappingURL=notification-service.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../utils/logger.js';
2
+ const SISYPHUS_SPINNER = ['·', '•', '●', '○', '◌', '◦', ' '];
2
3
  let instance;
3
4
  let tui = null;
4
5
  let initialized = false;
@@ -20,15 +21,30 @@ export class NotificationService {
20
21
  isReady() {
21
22
  return initialized && tui !== null;
22
23
  }
23
- showToast(message, type = 'info') {
24
- const title = this.getTitleForType(type);
24
+ showToast(options, type) {
25
+ let title;
26
+ let message;
27
+ let variant;
28
+ let duration;
29
+ if (typeof options === 'string') {
30
+ message = options;
31
+ variant = type || 'info';
32
+ title = this.getTitleForType(variant);
33
+ duration = variant === 'error' ? 8000 : 5000;
34
+ }
35
+ else {
36
+ message = options.message;
37
+ variant = options.variant || 'info';
38
+ title = options.title || this.getTitleForType(variant);
39
+ duration = options.duration || (variant === 'error' ? 8000 : 5000);
40
+ }
25
41
  if (tui?.showToast) {
26
42
  try {
27
43
  tui.showToast({
28
44
  title,
29
45
  message,
30
- variant: type,
31
- duration: type === 'error' ? 8000 : 5000
46
+ variant,
47
+ duration
32
48
  });
33
49
  return;
34
50
  }
@@ -36,12 +52,46 @@ export class NotificationService {
36
52
  logger.warn({ err }, 'tui.showToast failed');
37
53
  }
38
54
  }
39
- this.fallbackLog(type, title, message);
55
+ this.fallbackLog(variant, title, message);
56
+ }
57
+ success(message, duration) {
58
+ this.showToast({ message, variant: 'success', duration });
59
+ }
60
+ error(message, duration) {
61
+ this.showToast({ message, variant: 'error', duration: duration || 8000 });
62
+ }
63
+ warning(message, duration) {
64
+ this.showToast({ message, variant: 'warning', duration });
65
+ }
66
+ info(message, duration) {
67
+ this.showToast({ message, variant: 'info', duration });
68
+ }
69
+ async showSpinnerToast(title, message, duration = 2000) {
70
+ const frameInterval = 100;
71
+ const totalFrames = Math.ceil(duration / frameInterval);
72
+ for (let i = 0; i < totalFrames; i++) {
73
+ const spinner = SISYPHUS_SPINNER[i % SISYPHUS_SPINNER.length];
74
+ const toastDuration = Math.min(frameInterval + 50, duration - i * frameInterval);
75
+ if (toastDuration <= 0)
76
+ break;
77
+ if (tui?.showToast) {
78
+ try {
79
+ await tui.showToast({
80
+ title: `${spinner} ${title}`,
81
+ message,
82
+ variant: 'info',
83
+ duration: toastDuration
84
+ });
85
+ }
86
+ catch (err) {
87
+ logger.warn({ err }, 'tui.showSpinnerToast failed');
88
+ }
89
+ }
90
+ if (i < totalFrames - 1) {
91
+ await new Promise((resolve) => setTimeout(resolve, frameInterval));
92
+ }
93
+ }
40
94
  }
41
- success(message) { this.showToast(message, 'success'); }
42
- error(message) { this.showToast(message, 'error'); }
43
- warning(message) { this.showToast(message, 'warning'); }
44
- info(message) { this.showToast(message, 'info'); }
45
95
  getTitleForType(type) {
46
96
  const titles = {
47
97
  success: 'Success',
@@ -65,8 +115,8 @@ export class NotificationService {
65
115
  }
66
116
  }
67
117
  }
68
- export function showToast(message, type = 'info') {
69
- NotificationService.getInstance().showToast(message, type);
118
+ export function showToast(options, type) {
119
+ NotificationService.getInstance().showToast(options, type);
70
120
  }
71
121
  export function initializeNotificationService(tuiInstance) {
72
122
  NotificationService.getInstance().initialize(tuiInstance);
@@ -5,9 +5,8 @@ export interface speakerServiceOptions {
5
5
  defaultVoice?: string;
6
6
  }
7
7
  export declare class SpeakerService {
8
- private options;
9
8
  private speaker;
10
- constructor(options?: speakerServiceOptions);
9
+ constructor(_options?: speakerServiceOptions);
11
10
  speak(text: string, options?: SpeakOptions & {
12
11
  provider?: string;
13
12
  }): Promise<void>;
@@ -1,10 +1,8 @@
1
1
  import { getDefaultSpeaker, speak as coreSpeak, stop as coreStop, pause as corePause, resume as coreResume, listVoices as coreListVoices } from '../core/speaker';
2
2
  import { logger } from '../utils/logger';
3
3
  export class SpeakerService {
4
- options;
5
4
  speaker;
6
- constructor(options = {}) {
7
- this.options = options;
5
+ constructor(_options = {}) {
8
6
  this.speaker = getDefaultSpeaker();
9
7
  }
10
8
  async speak(text, options) {
@@ -117,7 +117,7 @@ export class StreamingService extends EventEmitter {
117
117
  }
118
118
  // 初始化播放器
119
119
  const player = this.initPlayer();
120
- player.start();
120
+ await player.start();
121
121
  this._isActive = true;
122
122
  this._bytesWritten = 0;
123
123
  // 调用 Provider 的流式合成
@@ -149,7 +149,7 @@ export class StreamingService extends EventEmitter {
149
149
  }
150
150
  else if (Buffer.isBuffer(result.audioData)) {
151
151
  // 非流式数据:直接写入
152
- player.write(result.audioData);
152
+ await player.write(result.audioData);
153
153
  player.end();
154
154
  }
155
155
  }
@@ -166,7 +166,7 @@ export class StreamingService extends EventEmitter {
166
166
  }
167
167
  if (value) {
168
168
  const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
169
- player.write(chunk);
169
+ await player.write(chunk);
170
170
  }
171
171
  }
172
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/ocosay",
3
- "version": "1.1.33",
3
+ "version": "1.2.0",
4
4
  "description": "OpenCode TTS 播放插件 - 支持豆包模式边接收边朗读",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",