@huyooo/ai-chat-bridge-electron 0.2.15 → 0.2.17
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/package.json +5 -6
- package/src/main/asr/client.ts +0 -270
- package/src/main/asr/index.ts +0 -270
- package/src/main/asr/protocol.ts +0 -344
- package/src/main/index.ts +0 -639
- package/src/preload/index.ts +0 -584
- package/src/renderer/index.ts +0 -674
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huyooo/ai-chat-bridge-electron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "AI Chat Electron Bridge - IPC integration for Electron apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/main/index.js",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"dist"
|
|
28
|
-
"src"
|
|
27
|
+
"dist"
|
|
29
28
|
],
|
|
30
29
|
"scripts": {
|
|
31
30
|
"build": "tsup",
|
|
@@ -34,9 +33,9 @@
|
|
|
34
33
|
"clean": "rm -rf dist"
|
|
35
34
|
},
|
|
36
35
|
"dependencies": {
|
|
37
|
-
"@huyooo/ai-chat-core": "^0.2.
|
|
38
|
-
"@huyooo/ai-chat-storage": "^0.2.
|
|
39
|
-
"@huyooo/ai-search": "^0.2.
|
|
36
|
+
"@huyooo/ai-chat-core": "^0.2.17",
|
|
37
|
+
"@huyooo/ai-chat-storage": "^0.2.17",
|
|
38
|
+
"@huyooo/ai-search": "^0.2.17",
|
|
40
39
|
"uuid": "^11.1.0",
|
|
41
40
|
"ws": "^8.18.3"
|
|
42
41
|
},
|
package/src/main/asr/client.ts
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 火山引擎大模型流式语音识别 - WebSocket 客户端
|
|
3
|
-
*
|
|
4
|
-
* 在 Electron 主进程中运行,处理与火山引擎 ASR 服务的通信
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import WebSocket from 'ws';
|
|
8
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
-
import {
|
|
10
|
-
encodeFullClientRequest,
|
|
11
|
-
encodeAudioOnlyRequest,
|
|
12
|
-
decodeServerResponse,
|
|
13
|
-
type AsrRequestConfig,
|
|
14
|
-
type AsrResult,
|
|
15
|
-
} from './protocol';
|
|
16
|
-
|
|
17
|
-
// ============ 类型定义 ============
|
|
18
|
-
|
|
19
|
-
/** ASR 客户端配置 */
|
|
20
|
-
export interface AsrClientConfig {
|
|
21
|
-
/** 火山引擎 App ID */
|
|
22
|
-
appId: string;
|
|
23
|
-
/** 火山引擎 Access Key */
|
|
24
|
-
accessKey: string;
|
|
25
|
-
/** 资源 ID */
|
|
26
|
-
resourceId?: string;
|
|
27
|
-
/** 使用双向流式优化版 */
|
|
28
|
-
useAsyncMode?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** ASR 事件回调 */
|
|
32
|
-
export interface AsrCallbacks {
|
|
33
|
-
/** 连接成功 */
|
|
34
|
-
onConnected?: () => void;
|
|
35
|
-
/** 收到识别结果 */
|
|
36
|
-
onResult?: (result: AsrResult, isLast: boolean) => void;
|
|
37
|
-
/** 发生错误 */
|
|
38
|
-
onError?: (error: Error) => void;
|
|
39
|
-
/** 连接关闭 */
|
|
40
|
-
onClose?: () => void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** ASR 会话配置 */
|
|
44
|
-
export interface AsrSessionConfig {
|
|
45
|
-
/** 音频格式 */
|
|
46
|
-
format?: 'pcm' | 'wav';
|
|
47
|
-
/** 采样率 */
|
|
48
|
-
sampleRate?: number;
|
|
49
|
-
/** 启用标点 */
|
|
50
|
-
enablePunc?: boolean;
|
|
51
|
-
/** 启用 ITN(数字规整) */
|
|
52
|
-
enableItn?: boolean;
|
|
53
|
-
/** 启用语义顺滑 */
|
|
54
|
-
enableDdc?: boolean;
|
|
55
|
-
/** 输出分句信息 */
|
|
56
|
-
showUtterances?: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ============ ASR 客户端类 ============
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* ASR WebSocket 客户端
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```ts
|
|
66
|
-
* const client = new AsrClient({
|
|
67
|
-
* appId: 'your-app-id',
|
|
68
|
-
* accessKey: 'your-access-key',
|
|
69
|
-
* });
|
|
70
|
-
*
|
|
71
|
-
* await client.connect({
|
|
72
|
-
* onResult: (result, isLast) => {
|
|
73
|
-
* console.log('识别结果:', result.result?.text);
|
|
74
|
-
* },
|
|
75
|
-
* onError: (error) => {
|
|
76
|
-
* console.error('错误:', error);
|
|
77
|
-
* },
|
|
78
|
-
* });
|
|
79
|
-
*
|
|
80
|
-
* client.sendAudio(audioBuffer);
|
|
81
|
-
* client.finish(); // 发送最后一包
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export class AsrClient {
|
|
85
|
-
private config: AsrClientConfig;
|
|
86
|
-
private ws: WebSocket | null = null;
|
|
87
|
-
private callbacks: AsrCallbacks = {};
|
|
88
|
-
private connectId: string = '';
|
|
89
|
-
private isConnected = false;
|
|
90
|
-
private sessionConfig: AsrSessionConfig = {};
|
|
91
|
-
|
|
92
|
-
constructor(config: AsrClientConfig) {
|
|
93
|
-
this.config = config;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 获取 WebSocket 连接地址
|
|
98
|
-
*/
|
|
99
|
-
private getWsUrl(): string {
|
|
100
|
-
// 默认使用双向流式优化版
|
|
101
|
-
if (this.config.useAsyncMode !== false) {
|
|
102
|
-
return 'wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async';
|
|
103
|
-
}
|
|
104
|
-
return 'wss://openspeech.bytedance.com/api/v3/sauc/bigmodel';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 获取资源 ID
|
|
109
|
-
*/
|
|
110
|
-
private getResourceId(): string {
|
|
111
|
-
return this.config.resourceId || 'volc.bigasr.sauc.duration';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 连接到 ASR 服务
|
|
116
|
-
*/
|
|
117
|
-
connect(callbacks: AsrCallbacks, sessionConfig: AsrSessionConfig = {}): Promise<void> {
|
|
118
|
-
return new Promise((resolve, reject) => {
|
|
119
|
-
this.callbacks = callbacks;
|
|
120
|
-
this.sessionConfig = sessionConfig;
|
|
121
|
-
this.connectId = uuidv4();
|
|
122
|
-
|
|
123
|
-
const wsUrl = this.getWsUrl();
|
|
124
|
-
|
|
125
|
-
this.ws = new WebSocket(wsUrl, {
|
|
126
|
-
headers: {
|
|
127
|
-
'X-Api-App-Key': this.config.appId,
|
|
128
|
-
'X-Api-Access-Key': this.config.accessKey,
|
|
129
|
-
'X-Api-Resource-Id': this.getResourceId(),
|
|
130
|
-
'X-Api-Connect-Id': this.connectId,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
this.ws.on('open', () => {
|
|
135
|
-
console.log('[ASR] WebSocket 连接成功, connectId:', this.connectId);
|
|
136
|
-
this.isConnected = true;
|
|
137
|
-
|
|
138
|
-
// 发送首包(Full Client Request)
|
|
139
|
-
this.sendFullClientRequest();
|
|
140
|
-
|
|
141
|
-
this.callbacks.onConnected?.();
|
|
142
|
-
resolve();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
this.ws.on('message', (data: Buffer) => {
|
|
146
|
-
try {
|
|
147
|
-
const response = decodeServerResponse(data);
|
|
148
|
-
|
|
149
|
-
if (response.type === 'error') {
|
|
150
|
-
const errorData = response.data as { code: number; message: string };
|
|
151
|
-
console.error('[ASR] 服务端错误:', errorData);
|
|
152
|
-
this.callbacks.onError?.(new Error(`ASR Error ${errorData.code}: ${errorData.message}`));
|
|
153
|
-
} else {
|
|
154
|
-
const result = response.data as AsrResult;
|
|
155
|
-
this.callbacks.onResult?.(result, response.isLast);
|
|
156
|
-
|
|
157
|
-
if (response.isLast) {
|
|
158
|
-
console.log('[ASR] 收到最终结果');
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch (error) {
|
|
162
|
-
console.error('[ASR] 解析响应失败:', error);
|
|
163
|
-
this.callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
this.ws.on('error', (error) => {
|
|
168
|
-
console.error('[ASR] WebSocket 错误:', error);
|
|
169
|
-
this.callbacks.onError?.(error);
|
|
170
|
-
reject(error);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
this.ws.on('close', () => {
|
|
174
|
-
console.log('[ASR] WebSocket 连接关闭');
|
|
175
|
-
this.isConnected = false;
|
|
176
|
-
this.ws = null;
|
|
177
|
-
this.callbacks.onClose?.();
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* 发送首包(Full Client Request)
|
|
184
|
-
*/
|
|
185
|
-
private sendFullClientRequest(): void {
|
|
186
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
187
|
-
console.error('[ASR] WebSocket 未连接');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const config: AsrRequestConfig = {
|
|
192
|
-
user: {
|
|
193
|
-
uid: 'ai-chat-user',
|
|
194
|
-
},
|
|
195
|
-
audio: {
|
|
196
|
-
format: this.sessionConfig.format || 'pcm',
|
|
197
|
-
rate: this.sessionConfig.sampleRate || 16000,
|
|
198
|
-
bits: 16,
|
|
199
|
-
channel: 1,
|
|
200
|
-
},
|
|
201
|
-
request: {
|
|
202
|
-
model_name: 'bigmodel',
|
|
203
|
-
enable_itn: this.sessionConfig.enableItn ?? true,
|
|
204
|
-
enable_punc: this.sessionConfig.enablePunc ?? true,
|
|
205
|
-
enable_ddc: this.sessionConfig.enableDdc ?? false,
|
|
206
|
-
show_utterances: this.sessionConfig.showUtterances ?? true,
|
|
207
|
-
result_type: 'full',
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const packet = encodeFullClientRequest(config);
|
|
212
|
-
this.ws.send(packet);
|
|
213
|
-
console.log('[ASR] 已发送首包');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* 发送音频数据
|
|
218
|
-
* @param audioData PCM 音频数据(16bit, 16kHz, 单声道)
|
|
219
|
-
*/
|
|
220
|
-
sendAudio(audioData: Buffer): void {
|
|
221
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
222
|
-
console.error('[ASR] WebSocket 未连接,无法发送音频');
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const packet = encodeAudioOnlyRequest(audioData, false);
|
|
227
|
-
this.ws.send(packet);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 发送最后一包并结束识别
|
|
232
|
-
*/
|
|
233
|
-
finish(): void {
|
|
234
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
235
|
-
console.error('[ASR] WebSocket 未连接,无法结束');
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// 发送空的最后一包
|
|
240
|
-
const packet = encodeAudioOnlyRequest(Buffer.alloc(0), true);
|
|
241
|
-
this.ws.send(packet);
|
|
242
|
-
console.log('[ASR] 已发送结束包');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* 断开连接
|
|
247
|
-
*/
|
|
248
|
-
disconnect(): void {
|
|
249
|
-
if (this.ws) {
|
|
250
|
-
this.ws.close();
|
|
251
|
-
this.ws = null;
|
|
252
|
-
}
|
|
253
|
-
this.isConnected = false;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* 检查是否已连接
|
|
258
|
-
*/
|
|
259
|
-
get connected(): boolean {
|
|
260
|
-
return this.isConnected && this.ws?.readyState === WebSocket.OPEN;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* 创建 ASR 客户端实例
|
|
266
|
-
*/
|
|
267
|
-
export function createAsrClient(config: AsrClientConfig): AsrClient {
|
|
268
|
-
return new AsrClient(config);
|
|
269
|
-
}
|
|
270
|
-
|
package/src/main/asr/index.ts
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ASR 模块导出
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export * from './protocol';
|
|
6
|
-
export * from './client';
|
|
7
|
-
|
|
8
|
-
import { ipcMain, type IpcMainInvokeEvent, type WebContents } from 'electron';
|
|
9
|
-
import { AsrClient, type AsrClientConfig, type AsrSessionConfig } from './client';
|
|
10
|
-
import type { AsrResult } from './protocol';
|
|
11
|
-
|
|
12
|
-
// ============ 类型定义 ============
|
|
13
|
-
|
|
14
|
-
/** ASR 桥接配置 */
|
|
15
|
-
export interface AsrBridgeConfig {
|
|
16
|
-
/** IPC channel 前缀 */
|
|
17
|
-
channelPrefix?: string;
|
|
18
|
-
/** 火山引擎 App ID */
|
|
19
|
-
appId: string;
|
|
20
|
-
/** 火山引擎 Access Key */
|
|
21
|
-
accessKey: string;
|
|
22
|
-
/** 资源 ID */
|
|
23
|
-
resourceId?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** ASR 会话状态 */
|
|
27
|
-
interface AsrSession {
|
|
28
|
-
client: AsrClient;
|
|
29
|
-
webContents: WebContents;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ============ ASR IPC Handlers ============
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 创建 ASR Electron 桥接
|
|
36
|
-
*
|
|
37
|
-
* 在主进程中调用,注册 ASR 相关的 IPC handlers
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* import { createAsrBridge } from '@huyooo/ai-chat-bridge-electron/main';
|
|
42
|
-
*
|
|
43
|
-
* createAsrBridge({
|
|
44
|
-
* appId: process.env.VOLC_APP_ID!,
|
|
45
|
-
* accessKey: process.env.VOLC_ACCESS_KEY!,
|
|
46
|
-
* });
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export function createAsrBridge(config: AsrBridgeConfig) {
|
|
50
|
-
const { channelPrefix = 'ai-chat', appId, accessKey, resourceId } = config;
|
|
51
|
-
|
|
52
|
-
// 当前活跃的 ASR 会话(一个窗口同时只能有一个会话)
|
|
53
|
-
const sessions = new Map<number, AsrSession>();
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 获取或创建 ASR 会话
|
|
57
|
-
*/
|
|
58
|
-
function getOrCreateSession(webContentsId: number, webContents: WebContents): AsrSession {
|
|
59
|
-
let session = sessions.get(webContentsId);
|
|
60
|
-
|
|
61
|
-
if (!session) {
|
|
62
|
-
const clientConfig: AsrClientConfig = {
|
|
63
|
-
appId,
|
|
64
|
-
accessKey,
|
|
65
|
-
resourceId,
|
|
66
|
-
useAsyncMode: true, // 使用双向流式优化版
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const client = new AsrClient(clientConfig);
|
|
70
|
-
session = { client, webContents };
|
|
71
|
-
sessions.set(webContentsId, session);
|
|
72
|
-
|
|
73
|
-
// 当 webContents 销毁时清理会话
|
|
74
|
-
webContents.on('destroyed', () => {
|
|
75
|
-
const s = sessions.get(webContentsId);
|
|
76
|
-
if (s) {
|
|
77
|
-
s.client.disconnect();
|
|
78
|
-
sessions.delete(webContentsId);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return session;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ============ IPC Handlers ============
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 开始 ASR 会话
|
|
90
|
-
*/
|
|
91
|
-
ipcMain.handle(`${channelPrefix}:asr:start`, async (
|
|
92
|
-
event: IpcMainInvokeEvent,
|
|
93
|
-
sessionConfig?: AsrSessionConfig
|
|
94
|
-
) => {
|
|
95
|
-
const webContents = event.sender;
|
|
96
|
-
const webContentsId = webContents.id;
|
|
97
|
-
const session = getOrCreateSession(webContentsId, webContents);
|
|
98
|
-
|
|
99
|
-
// 如果已连接,先断开
|
|
100
|
-
if (session.client.connected) {
|
|
101
|
-
session.client.disconnect();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
await session.client.connect({
|
|
106
|
-
onConnected: () => {
|
|
107
|
-
if (!webContents.isDestroyed()) {
|
|
108
|
-
webContents.send(`${channelPrefix}:asr:connected`);
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
onResult: (result: AsrResult, isLast: boolean) => {
|
|
112
|
-
if (!webContents.isDestroyed()) {
|
|
113
|
-
webContents.send(`${channelPrefix}:asr:result`, { result, isLast });
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
onError: (error: Error) => {
|
|
117
|
-
if (!webContents.isDestroyed()) {
|
|
118
|
-
webContents.send(`${channelPrefix}:asr:error`, { message: error.message });
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
onClose: () => {
|
|
122
|
-
if (!webContents.isDestroyed()) {
|
|
123
|
-
webContents.send(`${channelPrefix}:asr:closed`);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
}, sessionConfig);
|
|
127
|
-
|
|
128
|
-
return { success: true };
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error('[ASR Bridge] 启动失败:', error);
|
|
131
|
-
return {
|
|
132
|
-
success: false,
|
|
133
|
-
error: error instanceof Error ? error.message : String(error),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* 发送音频数据
|
|
140
|
-
*/
|
|
141
|
-
ipcMain.handle(`${channelPrefix}:asr:sendAudio`, async (
|
|
142
|
-
event: IpcMainInvokeEvent,
|
|
143
|
-
audioData: ArrayBufferLike
|
|
144
|
-
) => {
|
|
145
|
-
const webContentsId = event.sender.id;
|
|
146
|
-
const session = sessions.get(webContentsId);
|
|
147
|
-
|
|
148
|
-
if (!session || !session.client.connected) {
|
|
149
|
-
return { success: false, error: 'ASR 会话未启动' };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const payload =
|
|
154
|
-
audioData instanceof ArrayBuffer
|
|
155
|
-
? Buffer.from(audioData)
|
|
156
|
-
: Buffer.from(new Uint8Array(audioData));
|
|
157
|
-
|
|
158
|
-
session.client.sendAudio(payload);
|
|
159
|
-
return { success: true };
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.error('[ASR Bridge] 发送音频失败:', error);
|
|
162
|
-
return {
|
|
163
|
-
success: false,
|
|
164
|
-
error: error instanceof Error ? error.message : String(error),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 结束 ASR 会话
|
|
171
|
-
*/
|
|
172
|
-
ipcMain.handle(`${channelPrefix}:asr:finish`, async (event: IpcMainInvokeEvent) => {
|
|
173
|
-
const webContentsId = event.sender.id;
|
|
174
|
-
const session = sessions.get(webContentsId);
|
|
175
|
-
|
|
176
|
-
if (!session || !session.client.connected) {
|
|
177
|
-
return { success: false, error: 'ASR 会话未启动' };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
session.client.finish();
|
|
182
|
-
return { success: true };
|
|
183
|
-
} catch (error) {
|
|
184
|
-
console.error('[ASR Bridge] 结束会话失败:', error);
|
|
185
|
-
return {
|
|
186
|
-
success: false,
|
|
187
|
-
error: error instanceof Error ? error.message : String(error),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* 停止 ASR 会话(强制断开)
|
|
194
|
-
*/
|
|
195
|
-
ipcMain.handle(`${channelPrefix}:asr:stop`, async (event: IpcMainInvokeEvent) => {
|
|
196
|
-
const webContentsId = event.sender.id;
|
|
197
|
-
const session = sessions.get(webContentsId);
|
|
198
|
-
|
|
199
|
-
if (session) {
|
|
200
|
-
session.client.disconnect();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return { success: true };
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 检查 ASR 会话状态
|
|
208
|
-
*/
|
|
209
|
-
ipcMain.handle(`${channelPrefix}:asr:status`, async (event: IpcMainInvokeEvent) => {
|
|
210
|
-
const webContentsId = event.sender.id;
|
|
211
|
-
const session = sessions.get(webContentsId);
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
connected: session?.client.connected ?? false,
|
|
215
|
-
};
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 预热 ASR 连接(connect -> disconnect)
|
|
220
|
-
* 用于减少首次语音识别启动时的冷启动耗时:
|
|
221
|
-
* - DNS/TLS/WS 首次握手
|
|
222
|
-
* - 模块加载/初始化
|
|
223
|
-
*
|
|
224
|
-
* 注意:不会发送 connected/result 事件给渲染进程,避免影响 UI。
|
|
225
|
-
*/
|
|
226
|
-
ipcMain.handle(`${channelPrefix}:asr:warmup`, async (
|
|
227
|
-
event: IpcMainInvokeEvent,
|
|
228
|
-
sessionConfig?: AsrSessionConfig
|
|
229
|
-
) => {
|
|
230
|
-
const webContents = event.sender;
|
|
231
|
-
const webContentsId = webContents.id;
|
|
232
|
-
const session = getOrCreateSession(webContentsId, webContents);
|
|
233
|
-
|
|
234
|
-
if (session.client.connected) return { success: true };
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
await session.client.connect(
|
|
238
|
-
{
|
|
239
|
-
onConnected: () => {},
|
|
240
|
-
onResult: () => {},
|
|
241
|
-
onError: () => {},
|
|
242
|
-
onClose: () => {},
|
|
243
|
-
},
|
|
244
|
-
sessionConfig
|
|
245
|
-
);
|
|
246
|
-
session.client.disconnect();
|
|
247
|
-
return { success: true };
|
|
248
|
-
} catch (error) {
|
|
249
|
-
// 预热失败不影响正常使用(下次 start 仍会重试)
|
|
250
|
-
console.warn('[ASR Bridge] warmup 失败:', error);
|
|
251
|
-
return {
|
|
252
|
-
success: false,
|
|
253
|
-
error: error instanceof Error ? error.message : String(error),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
console.log('[ASR Bridge] IPC handlers 已注册');
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
/** 清理所有会话 */
|
|
262
|
-
cleanup: () => {
|
|
263
|
-
for (const session of sessions.values()) {
|
|
264
|
-
session.client.disconnect();
|
|
265
|
-
}
|
|
266
|
-
sessions.clear();
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|