@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.
- package/dist/core/backends/index.js +39 -17
- package/dist/core/backends/playsound-backend.js +82 -48
- package/dist/core/backends/speaker-backend.js +2 -1
- package/dist/core/notification.d.ts +7 -1
- package/dist/core/notification.js +64 -11
- package/dist/core/player.js +1 -1
- package/dist/core/stream-player.d.ts +3 -2
- package/dist/core/stream-player.js +21 -9
- package/dist/index.js +2 -2
- package/dist/package.json +1 -1
- package/dist/plugin.js +237 -84
- package/dist/providers/minimax.d.ts +0 -1
- package/dist/providers/minimax.js +0 -1
- package/dist/services/notification-service.d.ts +13 -4
- package/dist/services/notification-service.js +61 -11
- package/dist/services/speaker-service.d.ts +1 -2
- package/dist/services/speaker-service.js +1 -3
- package/dist/services/streaming-service.js +3 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
+
playWithPlaySound(filePath) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
50
52
|
// 异步导入 play-sound
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ToastVariant = 'success' | 'error' | 'info' | 'warning';
|
|
2
2
|
export interface ToastOptions {
|
|
3
|
-
title
|
|
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
|
|
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(
|
|
72
|
-
|
|
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(
|
|
75
|
-
|
|
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(
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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();
|
package/dist/core/player.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
164
|
+
streamPlayer.stop();
|
|
165
165
|
streamPlayer = undefined;
|
|
166
166
|
}
|
|
167
167
|
if (speaker) {
|
package/dist/package.json
CHANGED
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
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
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
|
-
|
|
7523
|
-
}
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
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
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
}
|
|
7547
|
-
}
|
|
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.
|
|
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
|
|
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(
|
|
7830
|
-
|
|
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
|
-
|
|
7833
|
-
|
|
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
|
-
|
|
7836
|
-
|
|
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
|
-
|
|
7839
|
-
|
|
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
|
-
|
|
8302
|
-
|
|
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 || "
|
|
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.
|
|
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: (
|
|
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
|
-
|
|
10127
|
-
|
|
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
|
-
|
|
10612
|
+
`Ocosay v${pluginVersion} Init Failed
|
|
10613
|
+
Please check your config file`,
|
|
10461
10614
|
8e3
|
|
10462
10615
|
);
|
|
10463
10616
|
} else {
|
|
10464
|
-
notificationService.
|
|
10617
|
+
notificationService.showSpinnerToast(
|
|
10465
10618
|
`Ocosay v${pluginVersion} Ready`,
|
|
10466
|
-
|
|
10619
|
+
envInfo,
|
|
10467
10620
|
5e3
|
|
10468
10621
|
);
|
|
10469
10622
|
}
|
|
@@ -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(
|
|
24
|
-
|
|
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
|
|
31
|
-
duration
|
|
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(
|
|
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(
|
|
69
|
-
NotificationService.getInstance().showToast(
|
|
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(
|
|
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(
|
|
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
|
}
|