ibi-ai-talk 1.0.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.
@@ -0,0 +1,565 @@
1
+ // WebSocket消息处理模块
2
+ import { webSocketConnect } from "./ota-connector.js";
3
+ import { getConfig, saveConnectionUrls } from "./manager.js";
4
+ import { getAudioPlayer } from "./player.js";
5
+ import { getAudioRecorder } from "./recorder.js";
6
+ import {
7
+ getMcpTools,
8
+ executeMcpTool,
9
+ setWebSocket as setMcpWebSocket,
10
+ } from "./tools.js";
11
+
12
+ // WebSocket处理器类
13
+ export class WebSocketHandler {
14
+ constructor() {
15
+ this.websocket = null;
16
+ this.onConnectionStateChange = null;
17
+ this.onRecordButtonStateChange = null;
18
+ this.onSessionStateChange = null;
19
+ this.onSessionEmotionChange = null;
20
+ this.currentSessionId = null;
21
+ this.isRemoteSpeaking = false;
22
+ this.heartbeatTimer = null;
23
+
24
+ // 重连相关配置
25
+ this.reconnectConfig = {
26
+ maxRetries: 10, // 最大重连次数
27
+ baseDelay: 1000, // 基础重连延迟(ms)
28
+ maxDelay: 30000, // 最大重连延迟(ms)
29
+ retryCount: 0, // 当前重连次数
30
+ reconnectTimer: null, // 重连定时器
31
+ isReconnecting: false, // 是否正在重连中
32
+ manualDisconnect: false, // 是否手动断开连接
33
+ };
34
+ }
35
+
36
+ // 在 WebSocketHandler 类中添加
37
+ startHeartbeat() {
38
+ this.stopHeartbeat(); // 先清除之前的定时器
39
+ this.sendHeartbeat();
40
+ }
41
+
42
+ stopHeartbeat() {
43
+ if (this.heartbeatTimer) {
44
+ clearTimeout(this.heartbeatTimer);
45
+ this.heartbeatTimer = null;
46
+ }
47
+ }
48
+
49
+ sendHeartbeat() {
50
+ if (this.websocket?.readyState === WebSocket.OPEN) {
51
+ try {
52
+ this.websocket.send("1");
53
+ } catch (error) {
54
+ console.error("心跳发送失败:", error);
55
+ }
56
+ }
57
+
58
+ this.heartbeatTimer = setTimeout(() => {
59
+ this.sendHeartbeat();
60
+ }, 5000);
61
+ }
62
+
63
+ // 发送hello握手消息
64
+ async sendHelloMessage() {
65
+ if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN)
66
+ return false;
67
+
68
+ this.startHeartbeat(); // 启动心跳
69
+ try {
70
+ const config = getConfig();
71
+
72
+ const helloMessage = {
73
+ type: "hello",
74
+ device_id: config.deviceId,
75
+ device_name: config.deviceName,
76
+ device_mac: config.deviceMac,
77
+ token: config.token,
78
+ seat: localStorage.getItem("SEAT"),
79
+ features: {
80
+ mcp: true,
81
+ },
82
+ };
83
+
84
+ console.log("发送hello握手消息", "info");
85
+ this.websocket.send(JSON.stringify(helloMessage));
86
+
87
+ return new Promise((resolve) => {
88
+ const timeout = setTimeout(() => {
89
+ console.log("等待hello响应超时", "error");
90
+ console.log('提示: 请尝试点击"测试认证"按钮进行连接排查', "info");
91
+ resolve(false);
92
+ }, 5000);
93
+
94
+ const onMessageHandler = (event) => {
95
+ try {
96
+ const response = JSON.parse(event.data);
97
+ if (response.type === "hello" && response.session_id) {
98
+ console.log(
99
+ `服务器握手成功,会话ID: ${response.session_id}`,
100
+ "success"
101
+ );
102
+ // 握手成功后重置重连计数器
103
+ this.reconnectConfig.retryCount = 0;
104
+ clearTimeout(timeout);
105
+ this.websocket.removeEventListener("message", onMessageHandler);
106
+ resolve(true);
107
+ }
108
+ } catch (e) {
109
+ // 忽略非JSON消息
110
+ }
111
+ };
112
+
113
+ this.websocket.addEventListener("message", onMessageHandler);
114
+ });
115
+ } catch (error) {
116
+ console.log(`发送hello消息错误: ${error.message}`, "error");
117
+ return false;
118
+ }
119
+ }
120
+
121
+ // 处理文本消息
122
+ handleTextMessage(message) {
123
+ if (message.type === "hello") {
124
+ } else if (message.type === "tts") {
125
+ this.handleTTSMessage(message);
126
+ } else if (message.type === "audio") {
127
+ } else if (message.type === "stt") {
128
+ const event = new CustomEvent("wsSendMessage", {
129
+ detail: message,
130
+ });
131
+ window.dispatchEvent(event);
132
+ } else if (message.type === "llm") {
133
+ } else if (message.type === "mcp") {
134
+ this.handleMCPMessage(message);
135
+ } else if (message.type === "json_data") {
136
+ if (message.state === "drinks") {
137
+ const event = new CustomEvent("drinkListEvent", {
138
+ detail: message.data,
139
+ });
140
+ window.dispatchEvent(event);
141
+ } else if (message.state === "book") {
142
+ const event = new CustomEvent("bookListEvent", {
143
+ detail: message
144
+ });
145
+ window.dispatchEvent(event);
146
+ }
147
+ } else if (message.type === "view_action") {
148
+ const event = new CustomEvent("viewActionEvent", {
149
+ detail: message.state,
150
+ });
151
+ window.dispatchEvent(event);
152
+ } else {
153
+ console.log(`未知消息类型: ${message.type}`, "warning");
154
+ }
155
+ }
156
+
157
+ // 处理TTS消息
158
+ handleTTSMessage(message) {
159
+ if (message.state === "start") {
160
+ console.log("服务器开始发送语音", "info");
161
+ this.currentSessionId = message.session_id;
162
+ const event = new CustomEvent("startThink");
163
+ window.dispatchEvent(event);
164
+ this.isRemoteSpeaking = true;
165
+ if (this.onSessionStateChange) {
166
+ this.onSessionStateChange(true);
167
+ }
168
+ } else if (message.state === "sentence_start") {
169
+ const event = new CustomEvent("startVolic", {
170
+ detail: message.text,
171
+ });
172
+ window.dispatchEvent(event);
173
+ console.log(`服务器发送语音段: ${message.text}`, "info");
174
+ } else if (message.state === "sentence_end") {
175
+ console.log(`语音段结束: ${message.text}`, "info");
176
+ } else if (message.state === "stop") {
177
+ const event = new CustomEvent("stopVolic");
178
+ window.dispatchEvent(event);
179
+ console.log("服务器语音传输结束", "info");
180
+ this.isRemoteSpeaking = false;
181
+ if (this.onRecordButtonStateChange) {
182
+ this.onRecordButtonStateChange(false);
183
+ }
184
+ if (this.onSessionStateChange) {
185
+ this.onSessionStateChange(false);
186
+ }
187
+ }
188
+ }
189
+
190
+ // 处理MCP消息
191
+ handleMCPMessage(message) {
192
+ const payload = message.payload || {};
193
+ console.log(`服务器下发: ${JSON.stringify(message)}`, "info");
194
+
195
+ if (payload.method === "tools/list") {
196
+ const tools = getMcpTools();
197
+ const replyMessage = JSON.stringify({
198
+ session_id: message.session_id || "",
199
+ type: "mcp",
200
+ payload: {
201
+ jsonrpc: "2.0",
202
+ id: payload.id,
203
+ result: {
204
+ tools: tools,
205
+ },
206
+ },
207
+ });
208
+ console.log(`客户端上报: ${replyMessage}`, "info");
209
+ this.websocket.send(replyMessage);
210
+ console.log(`回复MCP工具列表: ${tools.length} 个工具`, "info");
211
+ } else if (payload.method === "tools/call") {
212
+ const toolName = payload.params?.name;
213
+ const toolArgs = payload.params?.arguments;
214
+
215
+ console.log(
216
+ `调用工具: ${toolName} 参数: ${JSON.stringify(toolArgs)}`,
217
+ "info"
218
+ );
219
+
220
+ const result = executeMcpTool(toolName, toolArgs);
221
+
222
+ const replyMessage = JSON.stringify({
223
+ session_id: message.session_id || "",
224
+ type: "mcp",
225
+ payload: {
226
+ jsonrpc: "2.0",
227
+ id: payload.id,
228
+ result: {
229
+ content: [
230
+ {
231
+ type: "text",
232
+ text: JSON.stringify(result),
233
+ },
234
+ ],
235
+ isError: false,
236
+ },
237
+ },
238
+ });
239
+
240
+ console.log(`客户端上报: ${replyMessage}`, "info");
241
+ this.websocket.send(replyMessage);
242
+ } else if (payload.method === "initialize") {
243
+ console.log(
244
+ `收到工具初始化请求: ${JSON.stringify(payload.params)}`,
245
+ "info"
246
+ );
247
+ } else {
248
+ console.log(`未知的MCP方法: ${payload.method}`, "warning");
249
+ }
250
+ }
251
+
252
+ // 处理二进制消息
253
+ async handleBinaryMessage(data) {
254
+ try {
255
+ let arrayBuffer;
256
+ if (data instanceof ArrayBuffer) {
257
+ arrayBuffer = data;
258
+ } else if (data instanceof Blob) {
259
+ arrayBuffer = await data.arrayBuffer();
260
+ console.log(
261
+ `收到Blob音频数据,大小: ${arrayBuffer.byteLength}字节`,
262
+ "debug"
263
+ );
264
+ } else {
265
+ console.log(`收到未知类型的二进制数据: ${typeof data}`, "warning");
266
+ return;
267
+ }
268
+
269
+ const opusData = new Uint8Array(arrayBuffer);
270
+ const audioPlayer = getAudioPlayer();
271
+ audioPlayer.enqueueAudioData(opusData);
272
+ } catch (error) {
273
+ console.log(`处理二进制消息出错: ${error.message}`, "error");
274
+ }
275
+ }
276
+
277
+ // 计算重连延迟(指数退避策略)
278
+ calculateReconnectDelay() {
279
+ // 指数退避 + 随机抖动,避免多个客户端同时重连
280
+ const delay = Math.min(
281
+ this.reconnectConfig.baseDelay *
282
+ Math.pow(2, this.reconnectConfig.retryCount),
283
+ this.reconnectConfig.maxDelay
284
+ );
285
+ // 添加±20%的随机抖动
286
+ const jitter = delay * 0.2 * (Math.random() - 0.5);
287
+ return Math.round(delay + jitter);
288
+ }
289
+
290
+ // 触发自动重连
291
+ triggerReconnect() {
292
+ // 如果是手动断开连接,不进行重连
293
+ if (this.reconnectConfig.manualDisconnect) {
294
+ console.log("手动断开连接,不进行自动重连", "info");
295
+ return;
296
+ }
297
+
298
+ // 检查是否达到最大重连次数
299
+ if (this.reconnectConfig.retryCount >= this.reconnectConfig.maxRetries) {
300
+ console.log(
301
+ `已达到最大重连次数(${this.reconnectConfig.maxRetries}),停止重连`,
302
+ "error"
303
+ );
304
+ this.reconnectConfig.isReconnecting = false;
305
+ if (this.onConnectionStateChange) {
306
+ this.onConnectionStateChange(false);
307
+ }
308
+ return;
309
+ }
310
+
311
+ // 计算重连延迟
312
+ const delay = this.calculateReconnectDelay();
313
+ this.reconnectConfig.retryCount++;
314
+
315
+ console.log(
316
+ `准备进行第${this.reconnectConfig.retryCount}次重连,延迟${delay}ms`,
317
+ "info"
318
+ );
319
+
320
+ // 设置重连定时器
321
+ this.reconnectConfig.reconnectTimer = setTimeout(async () => {
322
+ console.log(`开始第${this.reconnectConfig.retryCount}次重连`, "info");
323
+ try {
324
+ const success = await this.connect();
325
+ if (success) {
326
+ console.log("重连成功", "success");
327
+ this.reconnectConfig.isReconnecting = false;
328
+ } else {
329
+ console.log(
330
+ `第${this.reconnectConfig.retryCount}次重连失败`,
331
+ "error"
332
+ );
333
+ this.triggerReconnect();
334
+ }
335
+ } catch (error) {
336
+ console.log(`重连出错: ${error.message}`, "error");
337
+ this.triggerReconnect();
338
+ }
339
+ }, delay);
340
+ }
341
+
342
+ // 停止自动重连
343
+ stopReconnect() {
344
+ if (this.reconnectConfig.reconnectTimer) {
345
+ clearTimeout(this.reconnectConfig.reconnectTimer);
346
+ this.reconnectConfig.reconnectTimer = null;
347
+ }
348
+ this.reconnectConfig.isReconnecting = false;
349
+ this.reconnectConfig.retryCount = 0;
350
+ }
351
+
352
+ // 连接WebSocket服务器
353
+ async connect() {
354
+ // 如果正在重连中,先停止之前的重连
355
+ this.stopReconnect();
356
+
357
+ const config = getConfig();
358
+ console.log("正在检查OTA状态...", "info");
359
+ saveConnectionUrls();
360
+
361
+ try {
362
+ const otaUrl = localStorage.getItem("xz_tester_otaUrl");
363
+ const ws = await webSocketConnect(otaUrl, config);
364
+ if (ws === undefined) {
365
+ // 连接失败,触发重连
366
+ if (
367
+ !this.reconnectConfig.isReconnecting &&
368
+ !this.reconnectConfig.manualDisconnect
369
+ ) {
370
+ this.reconnectConfig.isReconnecting = true;
371
+ this.triggerReconnect();
372
+ }
373
+ return false;
374
+ }
375
+
376
+ this.websocket = ws;
377
+
378
+ // 设置接收二进制数据的类型为ArrayBuffer
379
+ this.websocket.binaryType = "arraybuffer";
380
+
381
+ // 设置 MCP 模块的 WebSocket 实例
382
+ setMcpWebSocket(this.websocket);
383
+
384
+ // 设置录音器的WebSocket
385
+ const audioRecorder = getAudioRecorder();
386
+ audioRecorder.setWebSocket(this.websocket);
387
+
388
+ this.setupEventHandlers();
389
+
390
+ return true;
391
+ } catch (error) {
392
+ console.log(`连接错误: ${error.message}`, "error");
393
+ if (this.onConnectionStateChange) {
394
+ this.onConnectionStateChange(false);
395
+ }
396
+
397
+ // 连接出错,触发重连
398
+ if (
399
+ !this.reconnectConfig.isReconnecting &&
400
+ !this.reconnectConfig.manualDisconnect
401
+ ) {
402
+ this.reconnectConfig.isReconnecting = true;
403
+ this.triggerReconnect();
404
+ }
405
+
406
+ return false;
407
+ }
408
+ }
409
+
410
+ // 设置事件处理器
411
+ setupEventHandlers() {
412
+ this.websocket.onopen = async () => {
413
+ const url = localStorage.getItem("xz_tester_wsUrl");
414
+ console.log(`已连接到服务器: ${url}`, "success");
415
+
416
+ if (this.onConnectionStateChange) {
417
+ this.onConnectionStateChange(true);
418
+ }
419
+
420
+ // 连接成功后,默认状态为聆听中
421
+ this.isRemoteSpeaking = false;
422
+ if (this.onSessionStateChange) {
423
+ this.onSessionStateChange(false);
424
+ }
425
+
426
+ await this.sendHelloMessage();
427
+ };
428
+
429
+ this.websocket.onclose = (event) => {
430
+ console.log(
431
+ `已断开连接,代码: ${event.code}, 原因: ${event.reason}`,
432
+ "info"
433
+ );
434
+ this.stopHeartbeat();
435
+
436
+ // 清除MCP的WebSocket引用
437
+ setMcpWebSocket(null);
438
+
439
+ // 停止录音
440
+ const audioRecorder = getAudioRecorder();
441
+ audioRecorder.stop();
442
+
443
+ if (this.onConnectionStateChange) {
444
+ this.onConnectionStateChange(false);
445
+ }
446
+
447
+ // 如果不是手动断开连接,触发自动重连
448
+ if (
449
+ !this.reconnectConfig.manualDisconnect &&
450
+ !this.reconnectConfig.isReconnecting
451
+ ) {
452
+ // 1000: 正常关闭, 1001: 客户端离开, 这两种情况不自动重连
453
+ if (event.code !== 1000 && event.code !== 1001) {
454
+ console.log("检测到异常断开连接,准备自动重连", "warning");
455
+ this.reconnectConfig.isReconnecting = true;
456
+ this.triggerReconnect();
457
+ }
458
+ }
459
+
460
+ // 重置手动断开标记(以便下次可以重连)
461
+ this.reconnectConfig.manualDisconnect = false;
462
+ };
463
+
464
+ this.websocket.onerror = (error) => {
465
+ console.log(`WebSocket错误: ${error.message || "未知错误"}`, "error");
466
+
467
+ if (this.onConnectionStateChange) {
468
+ this.onConnectionStateChange(false);
469
+ }
470
+ };
471
+
472
+ this.websocket.onmessage = (event) => {
473
+ try {
474
+ if (typeof event.data === "string") {
475
+ const message = JSON.parse(event.data);
476
+ this.handleTextMessage(message);
477
+ } else {
478
+ this.handleBinaryMessage(event.data);
479
+ }
480
+ } catch (error) {
481
+ console.log(`WebSocket消息处理错误: ${error.message}`, "error");
482
+ }
483
+ };
484
+ }
485
+
486
+ // 断开连接
487
+ disconnect() {
488
+ // 标记为手动断开连接
489
+ this.reconnectConfig.manualDisconnect = true;
490
+ this.stopReconnect();
491
+
492
+ if (!this.websocket) return;
493
+
494
+ // 正常关闭连接
495
+ this.websocket.close(1000, "Manual disconnect");
496
+ const audioRecorder = getAudioRecorder();
497
+ audioRecorder.stop();
498
+ }
499
+
500
+ // 发送文本消息
501
+ sendTextMessage(text) {
502
+ try {
503
+ // 如果对方正在说话,先发送打断消息
504
+ const abortMessage = {
505
+ session_id: this.currentSessionId,
506
+ type: "abort",
507
+ reason: "wake_word_detected",
508
+ };
509
+ this.websocket.send(JSON.stringify(abortMessage));
510
+ console.log("发送打断消息", "info");
511
+
512
+ const listenMessage = {
513
+ type: "listen",
514
+ mode: localStorage.getItem("listenMode") || "wakeup",
515
+ state: "detect",
516
+ text: text,
517
+ };
518
+
519
+ this.websocket.send(JSON.stringify(listenMessage));
520
+ console.log(`发送文本消息: ${text}`, "info6666");
521
+
522
+ return true;
523
+ } catch (error) {
524
+ console.log(`发送消息错误: ${error.message}`, "error");
525
+ return false;
526
+ }
527
+ }
528
+
529
+ // 获取WebSocket实例
530
+ getWebSocket() {
531
+ return this.websocket;
532
+ }
533
+
534
+ // 检查是否已连接
535
+ isConnected() {
536
+ return this.websocket && this.websocket.readyState === WebSocket.OPEN;
537
+ }
538
+
539
+ // 获取重连状态
540
+ getReconnectStatus() {
541
+ return {
542
+ isReconnecting: this.reconnectConfig.isReconnecting,
543
+ retryCount: this.reconnectConfig.retryCount,
544
+ maxRetries: this.reconnectConfig.maxRetries,
545
+ };
546
+ }
547
+
548
+ // 重置重连配置
549
+ resetReconnectConfig() {
550
+ this.stopReconnect();
551
+ this.reconnectConfig.retryCount = 0;
552
+ this.reconnectConfig.isReconnecting = false;
553
+ this.reconnectConfig.manualDisconnect = false;
554
+ }
555
+ }
556
+
557
+ // 创建单例
558
+ let wsHandlerInstance = null;
559
+
560
+ export function getWebSocketHandler() {
561
+ if (!wsHandlerInstance) {
562
+ wsHandlerInstance = new WebSocketHandler();
563
+ }
564
+ return wsHandlerInstance;
565
+ }