@dofe/infra-clients 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/index.d.ts +0 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +0 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/sms/sms-aliyun.client.d.ts +2 -1
  6. package/dist/internal/sms/sms-aliyun.client.d.ts.map +1 -1
  7. package/dist/internal/sms/sms-volcengine.client.d.ts +2 -2
  8. package/package.json +72 -7
  9. package/dist/internal/file-cdn/dto/file-cdn.dto.d.ts +0 -78
  10. package/dist/internal/file-cdn/dto/file-cdn.dto.d.ts.map +0 -1
  11. package/dist/internal/file-cdn/dto/file-cdn.dto.js +0 -70
  12. package/dist/internal/file-cdn/dto/file-cdn.dto.js.map +0 -1
  13. package/dist/internal/file-cdn/file-cdn.client.d.ts +0 -283
  14. package/dist/internal/file-cdn/file-cdn.client.d.ts.map +0 -1
  15. package/dist/internal/file-cdn/file-cdn.client.js +0 -526
  16. package/dist/internal/file-cdn/file-cdn.client.js.map +0 -1
  17. package/dist/internal/file-cdn/file-cdn.module.d.ts +0 -3
  18. package/dist/internal/file-cdn/file-cdn.module.d.ts.map +0 -1
  19. package/dist/internal/file-cdn/file-cdn.module.js +0 -32
  20. package/dist/internal/file-cdn/file-cdn.module.js.map +0 -1
  21. package/dist/internal/file-cdn/index.d.ts +0 -35
  22. package/dist/internal/file-cdn/index.d.ts.map +0 -1
  23. package/dist/internal/file-cdn/index.js +0 -54
  24. package/dist/internal/file-cdn/index.js.map +0 -1
  25. package/dist/internal/openspeech/index.d.ts +0 -35
  26. package/dist/internal/openspeech/index.d.ts.map +0 -1
  27. package/dist/internal/openspeech/index.js +0 -56
  28. package/dist/internal/openspeech/index.js.map +0 -1
  29. package/dist/internal/openspeech/openspeech.client.d.ts +0 -304
  30. package/dist/internal/openspeech/openspeech.client.d.ts.map +0 -1
  31. package/dist/internal/openspeech/openspeech.client.js +0 -405
  32. package/dist/internal/openspeech/openspeech.client.js.map +0 -1
  33. package/dist/internal/openspeech/openspeech.factory.d.ts +0 -247
  34. package/dist/internal/openspeech/openspeech.factory.d.ts.map +0 -1
  35. package/dist/internal/openspeech/openspeech.factory.js +0 -406
  36. package/dist/internal/openspeech/openspeech.factory.js.map +0 -1
  37. package/dist/internal/openspeech/openspeech.module.d.ts +0 -45
  38. package/dist/internal/openspeech/openspeech.module.d.ts.map +0 -1
  39. package/dist/internal/openspeech/openspeech.module.js +0 -68
  40. package/dist/internal/openspeech/openspeech.module.js.map +0 -1
  41. package/dist/internal/openspeech/providers/aliyun.provider.d.ts +0 -125
  42. package/dist/internal/openspeech/providers/aliyun.provider.d.ts.map +0 -1
  43. package/dist/internal/openspeech/providers/aliyun.provider.js +0 -274
  44. package/dist/internal/openspeech/providers/aliyun.provider.js.map +0 -1
  45. package/dist/internal/openspeech/providers/base.provider.d.ts +0 -98
  46. package/dist/internal/openspeech/providers/base.provider.d.ts.map +0 -1
  47. package/dist/internal/openspeech/providers/base.provider.js +0 -87
  48. package/dist/internal/openspeech/providers/base.provider.js.map +0 -1
  49. package/dist/internal/openspeech/providers/index.d.ts +0 -10
  50. package/dist/internal/openspeech/providers/index.d.ts.map +0 -1
  51. package/dist/internal/openspeech/providers/index.js +0 -26
  52. package/dist/internal/openspeech/providers/index.js.map +0 -1
  53. package/dist/internal/openspeech/providers/volcengine-streaming.provider.d.ts +0 -291
  54. package/dist/internal/openspeech/providers/volcengine-streaming.provider.d.ts.map +0 -1
  55. package/dist/internal/openspeech/providers/volcengine-streaming.provider.js +0 -1358
  56. package/dist/internal/openspeech/providers/volcengine-streaming.provider.js.map +0 -1
  57. package/dist/internal/openspeech/providers/volcengine.provider.d.ts +0 -144
  58. package/dist/internal/openspeech/providers/volcengine.provider.d.ts.map +0 -1
  59. package/dist/internal/openspeech/providers/volcengine.provider.js +0 -337
  60. package/dist/internal/openspeech/providers/volcengine.provider.js.map +0 -1
  61. package/dist/internal/openspeech/types.d.ts +0 -408
  62. package/dist/internal/openspeech/types.d.ts.map +0 -1
  63. package/dist/internal/openspeech/types.js +0 -11
  64. package/dist/internal/openspeech/types.js.map +0 -1
  65. package/dist/internal/volcengine-tts/dto/tts.dto.d.ts +0 -52
  66. package/dist/internal/volcengine-tts/dto/tts.dto.d.ts.map +0 -1
  67. package/dist/internal/volcengine-tts/dto/tts.dto.js +0 -59
  68. package/dist/internal/volcengine-tts/dto/tts.dto.js.map +0 -1
  69. package/dist/internal/volcengine-tts/index.d.ts +0 -4
  70. package/dist/internal/volcengine-tts/index.d.ts.map +0 -1
  71. package/dist/internal/volcengine-tts/index.js +0 -20
  72. package/dist/internal/volcengine-tts/index.js.map +0 -1
  73. package/dist/internal/volcengine-tts/volcengine-tts.client.d.ts +0 -104
  74. package/dist/internal/volcengine-tts/volcengine-tts.client.d.ts.map +0 -1
  75. package/dist/internal/volcengine-tts/volcengine-tts.client.js +0 -690
  76. package/dist/internal/volcengine-tts/volcengine-tts.client.js.map +0 -1
  77. package/dist/internal/volcengine-tts/volcengine-tts.module.d.ts +0 -3
  78. package/dist/internal/volcengine-tts/volcengine-tts.module.d.ts.map +0 -1
  79. package/dist/internal/volcengine-tts/volcengine-tts.module.js +0 -34
  80. package/dist/internal/volcengine-tts/volcengine-tts.module.js.map +0 -1
@@ -1,1358 +0,0 @@
1
- "use strict";
2
- /**
3
- * @fileoverview 火山引擎流式语音识别 Provider
4
- *
5
- * 本文件实现了火山引擎大模型流式语音识别功能,支持实时音频流转写。
6
- * 火山引擎流式语音识别文档:https://www.volcengine.com/docs/6561/1354869
7
- *
8
- * 支持的模式:
9
- * - 双向流式模式(优化版本):wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async
10
- * 推荐使用,只有结果变化时才返回新数据包,性能更优
11
- *
12
- * @module openspeech/providers/volcengine-streaming
13
- */
14
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
- if (k2 === undefined) k2 = k;
16
- var desc = Object.getOwnPropertyDescriptor(m, k);
17
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
- desc = { enumerable: true, get: function() { return m[k]; } };
19
- }
20
- Object.defineProperty(o, k2, desc);
21
- }) : (function(o, m, k, k2) {
22
- if (k2 === undefined) k2 = k;
23
- o[k2] = m[k];
24
- }));
25
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
- Object.defineProperty(o, "default", { enumerable: true, value: v });
27
- }) : function(o, v) {
28
- o["default"] = v;
29
- });
30
- var __importStar = (this && this.__importStar) || (function () {
31
- var ownKeys = function(o) {
32
- ownKeys = Object.getOwnPropertyNames || function (o) {
33
- var ar = [];
34
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
- return ar;
36
- };
37
- return ownKeys(o);
38
- };
39
- return function (mod) {
40
- if (mod && mod.__esModule) return mod;
41
- var result = {};
42
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
- __setModuleDefault(result, mod);
44
- return result;
45
- };
46
- })();
47
- var __importDefault = (this && this.__importDefault) || function (mod) {
48
- return (mod && mod.__esModule) ? mod : { "default": mod };
49
- };
50
- Object.defineProperty(exports, "__esModule", { value: true });
51
- exports.VolcengineStreamingAsrProvider = void 0;
52
- const ws_1 = __importDefault(require("ws"));
53
- const zlib = __importStar(require("zlib"));
54
- const util_1 = require("util");
55
- const uuid_1 = require("uuid");
56
- const gzipAsync = (0, util_1.promisify)(zlib.gzip);
57
- const gunzipAsync = (0, util_1.promisify)(zlib.gunzip);
58
- /**
59
- * 消息类型常量
60
- * @description 定义 WebSocket 二进制协议中的消息类型
61
- */
62
- const MESSAGE_TYPE = {
63
- /** 端上发送包含请求参数的 full client request */
64
- FULL_CLIENT_REQUEST: 0b0001,
65
- /** 端上发送包含音频数据的 audio only request */
66
- AUDIO_ONLY_CLIENT_REQUEST: 0b0010,
67
- /** 服务端下发包含识别结果的 full server response */
68
- FULL_SERVER_RESPONSE: 0b1001,
69
- /** 服务端处理错误时下发的消息类型 */
70
- ERROR_RESPONSE: 0b1111,
71
- };
72
- /**
73
- * 消息类型特定标志
74
- */
75
- const MESSAGE_FLAGS = {
76
- /** header后4个字节不为sequence number */
77
- NO_SEQUENCE: 0b0000,
78
- /** header后4个字节为sequence number且为正 */
79
- POSITIVE_SEQUENCE: 0b0001,
80
- /** header后4个字节不为sequence number,仅指示此为最后一包(负包) */
81
- LAST_PACKET_NO_SEQ: 0b0010,
82
- /** header后4个字节为sequence number且需要为负数(最后一包/负包) */
83
- LAST_PACKET_WITH_SEQ: 0b0011,
84
- };
85
- /**
86
- * 序列化方法常量
87
- */
88
- const SERIALIZATION = {
89
- /** 无序列化 */
90
- NONE: 0b0000,
91
- /** JSON 格式 */
92
- JSON: 0b0001,
93
- };
94
- /**
95
- * 压缩方法常量
96
- */
97
- const COMPRESSION = {
98
- /** 无压缩 */
99
- NONE: 0b0000,
100
- /** Gzip 压缩 */
101
- GZIP: 0b0001,
102
- };
103
- /**
104
- * 火山引擎 ASR 错误码
105
- * @see https://www.volcengine.com/docs/6561/1354869
106
- */
107
- const VOLCENGINE_ERROR_CODES = {
108
- /** 成功 */
109
- SUCCESS: 20000000,
110
- /** 请求参数无效(缺失必需字段/字段值无效/重复请求) */
111
- INVALID_PARAMS: 45000001,
112
- /** 空音频 */
113
- EMPTY_AUDIO: 45000002,
114
- /** 等包超时 */
115
- PACKET_TIMEOUT: 45000081,
116
- /** 音频格式不正确 */
117
- INVALID_AUDIO_FORMAT: 45000151,
118
- /** 服务器繁忙(服务过载) */
119
- SERVER_BUSY: 55000031,
120
- };
121
- /**
122
- * 错误码到中文描述的映射
123
- */
124
- const ERROR_CODE_MESSAGES = {
125
- [VOLCENGINE_ERROR_CODES.SUCCESS]: '成功',
126
- [VOLCENGINE_ERROR_CODES.INVALID_PARAMS]: '请求参数无效:缺失必需字段、字段值无效或重复请求',
127
- [VOLCENGINE_ERROR_CODES.EMPTY_AUDIO]: '空音频:未收到有效的音频数据',
128
- [VOLCENGINE_ERROR_CODES.PACKET_TIMEOUT]: '等包超时:音频发送间隔过长',
129
- [VOLCENGINE_ERROR_CODES.INVALID_AUDIO_FORMAT]: '音频格式不正确:请检查音频编码格式',
130
- [VOLCENGINE_ERROR_CODES.SERVER_BUSY]: '服务器繁忙:服务过载,请稍后重试',
131
- };
132
- /**
133
- * 推荐的音频包大小(毫秒)
134
- * 文档建议 100-200ms,双向流式模式推荐 200ms
135
- */
136
- const RECOMMENDED_AUDIO_PACKET_MS = {
137
- MIN: 100,
138
- MAX: 200,
139
- OPTIMAL: 200,
140
- };
141
- /**
142
- * 默认重连配置
143
- */
144
- const DEFAULT_RECONNECT_CONFIG = {
145
- maxRetries: 3,
146
- initialDelay: 1000,
147
- maxDelay: 10000,
148
- backoffMultiplier: 2,
149
- };
150
- /**
151
- * 心跳配置
152
- */
153
- const HEARTBEAT_CONFIG = {
154
- /** 心跳间隔(毫秒) */
155
- interval: 25000,
156
- /** 心跳超时(毫秒) */
157
- timeout: 10000,
158
- };
159
- /**
160
- * 火山引擎流式语音识别 Provider
161
- *
162
- * @description 封装了火山引擎大模型流式语音识别的核心功能:
163
- * - WebSocket 连接管理
164
- * - 二进制协议编解码
165
- * - 音频数据分包发送
166
- * - 实时识别结果解析
167
- *
168
- * @class VolcengineStreamingAsrProvider
169
- * @implements {IStreamingAsrProvider}
170
- *
171
- * @example
172
- * ```typescript
173
- * const provider = new VolcengineStreamingAsrProvider(logger, config);
174
- *
175
- * // 建立连接
176
- * const connectionId = await provider.connect(
177
- * { sessionId: 'session-123', audioFormat: 'pcm' },
178
- * {
179
- * onResult: (result) => console.log('识别结果:', result.text),
180
- * onConnected: () => console.log('已连接'),
181
- * onError: (error) => console.error('错误:', error),
182
- * }
183
- * );
184
- *
185
- * // 发送音频数据
186
- * await provider.sendAudio(connectionId, audioBuffer);
187
- *
188
- * // 发送最后一帧
189
- * await provider.sendAudio(connectionId, lastAudioBuffer, true);
190
- *
191
- * // 关闭连接
192
- * await provider.disconnect(connectionId);
193
- * ```
194
- */
195
- class VolcengineStreamingAsrProvider {
196
- logger;
197
- config;
198
- /**
199
- * 云服务商标识
200
- */
201
- vendor = 'volcengine-streaming';
202
- /**
203
- * 连接池
204
- * @description 存储所有活跃的 WebSocket 连接
205
- */
206
- connections = new Map();
207
- /**
208
- * 重连配置
209
- */
210
- reconnectConfig;
211
- /**
212
- * 构造函数
213
- *
214
- * @param {Logger} logger - Winston 日志记录器
215
- * @param {VolcengineSaucConfig} config - 火山引擎流式语音识别配置
216
- * @param {Partial<ReconnectConfig>} reconnectConfig - 重连配置(可选)
217
- */
218
- constructor(logger, config, reconnectConfig) {
219
- this.logger = logger;
220
- this.config = config;
221
- if (!config) {
222
- throw new Error('Volcengine Streaming ASR config is required');
223
- }
224
- this.reconnectConfig = {
225
- ...DEFAULT_RECONNECT_CONFIG,
226
- ...reconnectConfig,
227
- };
228
- }
229
- /**
230
- * 记录信息日志
231
- */
232
- logInfo(message, meta) {
233
- this.logger.info(`[volcengine-streaming] ${message}`, meta);
234
- }
235
- /**
236
- * 记录错误日志
237
- */
238
- logError(message, meta) {
239
- this.logger.error(`[volcengine-streaming] ${message}`, meta);
240
- }
241
- /**
242
- * 记录警告日志
243
- */
244
- logWarn(message, meta) {
245
- this.logger.warn(`[volcengine-streaming] ${message}`, meta);
246
- }
247
- /**
248
- * 记录调试日志
249
- */
250
- logDebug(message, meta) {
251
- this.logger.debug(`[volcengine-streaming] ${message}`, meta);
252
- }
253
- /**
254
- * 构建 WebSocket 连接认证头
255
- *
256
- * @param connectId - 连接唯一标识
257
- * @returns HTTP 请求头
258
- */
259
- buildHeaders(connectId) {
260
- const headers = {
261
- 'X-Api-App-Key': this.config.appId,
262
- 'X-Api-Access-Key': this.config.appAccessToken,
263
- 'X-Api-Connect-Id': connectId,
264
- };
265
- // 可选的资源 ID
266
- if (this.config.resourceId) {
267
- headers['X-Api-Resource-Id'] = this.config.resourceId;
268
- }
269
- return headers;
270
- }
271
- /**
272
- * 构建消息头(4 bytes)
273
- *
274
- * @description 按照火山引擎 WebSocket 二进制协议构建消息头
275
- *
276
- * 协议格式:
277
- * - Byte 0: Version (4 bits) + Header Size (4 bits)
278
- * - Byte 1: Message Type (4 bits) + Message Type Specific Flags (4 bits)
279
- * - Byte 2: Serialization Method (4 bits) + Compression (4 bits)
280
- * - Byte 3: Reserved
281
- *
282
- * @param messageType - 消息类型
283
- * @param specificFlags - 消息类型特定标志
284
- * @param serialization - 序列化方法
285
- * @param compression - 压缩方法
286
- * @returns 4 字节的消息头 Buffer
287
- */
288
- buildMessageHeader(messageType, specificFlags = MESSAGE_FLAGS.NO_SEQUENCE, serialization = SERIALIZATION.JSON, compression = COMPRESSION.GZIP) {
289
- const header = Buffer.alloc(4);
290
- // Byte 0: Version (0b0001) + Header Size (0b0001 = 4 bytes)
291
- header[0] = (0b0001 << 4) | 0b0001;
292
- // Byte 1: Message Type + Specific Flags
293
- header[1] = (messageType << 4) | specificFlags;
294
- // Byte 2: Serialization + Compression
295
- header[2] = (serialization << 4) | compression;
296
- // Byte 3: Reserved
297
- header[3] = 0x00;
298
- return header;
299
- }
300
- /**
301
- * 构建完整客户端请求消息(初始请求)
302
- *
303
- * @param data - 请求参数(JSON 对象)
304
- * @returns 完整的二进制消息
305
- */
306
- async buildFullClientRequest(data) {
307
- const header = this.buildMessageHeader(MESSAGE_TYPE.FULL_CLIENT_REQUEST, MESSAGE_FLAGS.NO_SEQUENCE, SERIALIZATION.JSON, COMPRESSION.GZIP);
308
- const jsonData = JSON.stringify(data);
309
- const compressed = await gzipAsync(Buffer.from(jsonData, 'utf-8'));
310
- // Payload size (4 bytes, big-endian)
311
- const payloadSize = Buffer.alloc(4);
312
- payloadSize.writeUInt32BE(compressed.length, 0);
313
- return Buffer.concat([header, payloadSize, compressed]);
314
- }
315
- /**
316
- * 构建音频数据请求消息
317
- *
318
- * @param audioData - 音频数据
319
- * @param isLast - 是否为最后一帧
320
- * @returns 完整的二进制消息
321
- */
322
- async buildAudioOnlyRequest(audioData, isLast = false) {
323
- const specificFlags = isLast
324
- ? MESSAGE_FLAGS.LAST_PACKET_NO_SEQ
325
- : MESSAGE_FLAGS.NO_SEQUENCE;
326
- const header = this.buildMessageHeader(MESSAGE_TYPE.AUDIO_ONLY_CLIENT_REQUEST, specificFlags, SERIALIZATION.NONE, // 音频数据不序列化
327
- COMPRESSION.GZIP);
328
- const compressed = await gzipAsync(audioData);
329
- // Payload size (4 bytes, big-endian)
330
- const payloadSize = Buffer.alloc(4);
331
- payloadSize.writeUInt32BE(compressed.length, 0);
332
- return Buffer.concat([header, payloadSize, compressed]);
333
- }
334
- /**
335
- * 构建初始请求参数对象
336
- *
337
- * @description 根据连接参数构建符合火山引擎 API 规范的初始请求
338
- * @param options - 连接参数选项
339
- * @returns 初始请求对象
340
- */
341
- buildInitRequest(options) {
342
- const { audioFormat, sampleRate, channels, enableSpeakerInfo, language, enableItn, enablePunc, enableDdc, enableNonstream, showUtterances, showSpeechRate, showVolume, enableLid, enableEmotionDetection, enableGenderDetection, resultType, enableAccelerateText, accelerateScore, vadSegmentDuration, endWindowSize, forceToSpeechTime, sensitiveWordsFilter, corpus, } = options;
343
- // 构建 audio 配置
344
- const audio = {
345
- format: audioFormat,
346
- rate: sampleRate,
347
- bits: 16,
348
- channel: channels,
349
- };
350
- // 如果指定了语言,添加 language 字段
351
- if (language) {
352
- audio.language = language;
353
- }
354
- // 构建 request 配置
355
- const request = {
356
- model_name: 'bigmodel',
357
- enable_itn: enableItn,
358
- enable_punc: enablePunc,
359
- enable_ddc: enableDdc,
360
- enable_nonstream: enableNonstream,
361
- show_utterances: showUtterances,
362
- show_speech_rate: showSpeechRate,
363
- show_volume: showVolume,
364
- enable_lid: enableLid,
365
- enable_emotion_detection: enableEmotionDetection,
366
- enable_gender_detection: enableGenderDetection,
367
- enable_speaker_info: enableSpeakerInfo,
368
- result_type: resultType,
369
- enable_accelerate_text: enableAccelerateText,
370
- };
371
- // 可选参数
372
- if (accelerateScore !== undefined) {
373
- request.accelerate_score = accelerateScore;
374
- }
375
- if (vadSegmentDuration !== undefined) {
376
- request.vad_segment_duration = vadSegmentDuration;
377
- }
378
- if (endWindowSize !== undefined) {
379
- request.end_window_size = endWindowSize;
380
- }
381
- if (forceToSpeechTime !== undefined) {
382
- request.force_to_speech_time = forceToSpeechTime;
383
- }
384
- if (sensitiveWordsFilter) {
385
- request.sensitive_words_filter = sensitiveWordsFilter;
386
- }
387
- // 构建 corpus 配置(热词/干预词)
388
- if (corpus) {
389
- const corpusConfig = {};
390
- if (corpus.boostingTableName) {
391
- corpusConfig.boosting_table_name = corpus.boostingTableName;
392
- }
393
- if (corpus.boostingTableId) {
394
- corpusConfig.boosting_table_id = corpus.boostingTableId;
395
- }
396
- if (corpus.correctTableName) {
397
- corpusConfig.correct_table_name = corpus.correctTableName;
398
- }
399
- if (corpus.correctTableId) {
400
- corpusConfig.correct_table_id = corpus.correctTableId;
401
- }
402
- // 热词直传
403
- if (corpus.hotwords && corpus.hotwords.length > 0) {
404
- corpusConfig.context = JSON.stringify({
405
- hotwords: corpus.hotwords,
406
- });
407
- }
408
- if (Object.keys(corpusConfig).length > 0) {
409
- request.corpus = corpusConfig;
410
- }
411
- }
412
- return {
413
- user: {
414
- uid: this.config.uid,
415
- },
416
- audio,
417
- request,
418
- };
419
- }
420
- /**
421
- * 解析服务器响应
422
- *
423
- * @param data - 原始二进制响应数据
424
- * @returns 解析后的识别结果
425
- */
426
- async parseServerResponse(data) {
427
- if (data.length < 4) {
428
- throw new Error('Invalid response: too short');
429
- }
430
- // 解析消息头
431
- const messageType = (data[1] >> 4) & 0x0f;
432
- const specificFlags = data[1] & 0x0f;
433
- const compression = data[2] & 0x0f;
434
- // 处理错误响应
435
- if (messageType === MESSAGE_TYPE.ERROR_RESPONSE) {
436
- if (data.length < 12) {
437
- throw new Error('Invalid error response: too short');
438
- }
439
- const errorCode = data.readUInt32BE(4);
440
- const errorSize = data.readUInt32BE(8);
441
- // 验证 errorSize 的合理性(不超过 1MB)
442
- if (errorSize > 1024 * 1024) {
443
- this.logError('Invalid error size in error response', {
444
- errorSize,
445
- dataLength: data.length,
446
- firstBytes: data.slice(0, 16).toString('hex'),
447
- });
448
- throw new Error(`Invalid error response: errorSize too large (${errorSize})`);
449
- }
450
- if (data.length < 12 + errorSize) {
451
- throw new Error(`Incomplete error response: expected ${12 + errorSize} bytes, got ${data.length}`);
452
- }
453
- const rawErrorMessage = data.slice(12, 12 + errorSize).toString('utf-8');
454
- // 使用友好的错误描述
455
- const friendlyMessage = ERROR_CODE_MESSAGES[errorCode] || `未知错误 (${errorCode})`;
456
- this.logError('Volcengine ASR error response received', {
457
- errorCode,
458
- rawErrorMessage,
459
- friendlyMessage,
460
- });
461
- return {
462
- text: '',
463
- isFinal: true,
464
- error: `火山引擎 ASR 错误 [${errorCode}]: ${friendlyMessage}。原始信息: ${rawErrorMessage}`,
465
- errorCode,
466
- };
467
- }
468
- // 验证消息类型
469
- if (messageType !== MESSAGE_TYPE.FULL_SERVER_RESPONSE) {
470
- this.logWarn('Unexpected message type', {
471
- messageType,
472
- expected: MESSAGE_TYPE.FULL_SERVER_RESPONSE,
473
- dataLength: data.length,
474
- firstBytes: data.slice(0, 16).toString('hex'),
475
- });
476
- throw new Error(`Unexpected message type: ${messageType}`);
477
- }
478
- // 根据 specificFlags 判断是否有 sequence 字段
479
- // 0b0001 (POSITIVE_SEQUENCE) 和 0b0011 (LAST_PACKET_WITH_SEQ) 包含 sequence
480
- // 0b0000 (NO_SEQUENCE) 和 0b0010 (LAST_PACKET_NO_SEQ) 不包含 sequence
481
- const hasSequence = specificFlags === MESSAGE_FLAGS.POSITIVE_SEQUENCE ||
482
- specificFlags === MESSAGE_FLAGS.LAST_PACKET_WITH_SEQ;
483
- // 从 header 之后开始解析(header 固定 4 bytes)
484
- let offset = 4;
485
- // 解析序列号(如果存在)
486
- let sequence;
487
- if (hasSequence) {
488
- // 验证数据长度:至少需要 8 字节(header 4 + sequence 4)
489
- if (data.length < offset + 4) {
490
- throw new Error(`Invalid response: expected at least ${offset + 4} bytes for sequence, got ${data.length}`);
491
- }
492
- // 使用 readInt32BE 读取有符号整数(LAST_PACKET_WITH_SEQ 时 sequence 为负数)
493
- sequence = data.readInt32BE(offset);
494
- offset += 4;
495
- }
496
- // 解析 payload size(紧跟在 header 或 sequence 之后)
497
- if (data.length < offset + 4) {
498
- throw new Error(`Invalid response: expected at least ${offset + 4} bytes for payload size, got ${data.length}`);
499
- }
500
- const payloadSize = data.readUInt32BE(offset);
501
- offset += 4;
502
- // 验证 payloadSize 的合理性(不超过 10MB,防止读取错误位置导致异常大的值)
503
- const MAX_PAYLOAD_SIZE = 10 * 1024 * 1024; // 10MB
504
- if (payloadSize > MAX_PAYLOAD_SIZE) {
505
- this.logError('Invalid payload size', {
506
- payloadSize,
507
- dataLength: data.length,
508
- sequence,
509
- messageType,
510
- specificFlags,
511
- hasSequence,
512
- firstBytes: data.slice(0, 16).toString('hex'),
513
- });
514
- throw new Error(`Invalid payload size: ${payloadSize} bytes (max: ${MAX_PAYLOAD_SIZE}). ` +
515
- `This may indicate a parsing error or corrupted message.`);
516
- }
517
- // 验证数据完整性:确保有足够的数据来读取 payload
518
- const expectedLength = offset + payloadSize;
519
- if (data.length < expectedLength) {
520
- this.logWarn('Incomplete message detected', {
521
- expectedLength,
522
- actualLength: data.length,
523
- payloadSize,
524
- sequence,
525
- specificFlags,
526
- hasSequence,
527
- firstBytes: data.slice(0, 16).toString('hex'),
528
- });
529
- throw new Error(`Incomplete message: expected ${expectedLength} bytes, got ${data.length}. ` +
530
- `This may indicate a fragmented WebSocket message.`);
531
- }
532
- const payload = data.slice(offset, offset + payloadSize);
533
- // 解压 payload
534
- let jsonBuffer;
535
- try {
536
- if (compression === COMPRESSION.GZIP) {
537
- jsonBuffer = await gunzipAsync(payload);
538
- }
539
- else {
540
- jsonBuffer = payload;
541
- }
542
- }
543
- catch (decompressError) {
544
- throw new Error(`Failed to decompress payload: ${decompressError.message}`);
545
- }
546
- // 安全地解析 JSON
547
- let result;
548
- try {
549
- const jsonString = jsonBuffer.toString('utf-8');
550
- result = JSON.parse(jsonString);
551
- }
552
- catch (parseError) {
553
- // 提供更详细的错误信息,包括部分 JSON 内容
554
- const partialJson = jsonBuffer.toString('utf-8').substring(0, 100);
555
- throw new Error(`Failed to parse JSON: ${parseError.message}. ` +
556
- `Partial content: ${partialJson}...`);
557
- }
558
- // 提取音频时长信息
559
- const audioDuration = result.audio_info?.duration;
560
- // 提取识别结果
561
- const text = result.result?.text || result.result?.[0]?.text || '';
562
- const rawUtterances = result.result?.utterances || result.result?.[0]?.utterances || [];
563
- // 格式化 utterances(说话人分离数据)
564
- const utterances = rawUtterances.map((u, index) => {
565
- // 解析分词信息
566
- const rawWords = u.words || [];
567
- const words = rawWords.map((w) => ({
568
- text: w.text || '',
569
- startTime: w.start_time ?? w.startTime ?? 0,
570
- endTime: w.end_time ?? w.endTime ?? 0,
571
- blankDuration: w.blank_duration ?? w.blankDuration,
572
- }));
573
- return {
574
- speakerId: u.additions?.speaker ||
575
- u.speaker_id ||
576
- u.speakerId ||
577
- `speaker_${index}`,
578
- text: u.text || u.content || '',
579
- startTime: u.start_time ?? u.startTime ?? 0,
580
- endTime: u.end_time ?? u.endTime ?? 0,
581
- definite: u.definite ?? true,
582
- speechRate: u.additions?.speech_rate ?? u.speech_rate ?? undefined,
583
- volume: u.additions?.volume ?? u.volume ?? undefined,
584
- emotion: u.additions?.emotion ?? u.emotion ?? undefined,
585
- gender: u.additions?.gender ?? u.gender ?? undefined,
586
- language: u.additions?.language ?? u.language ?? undefined,
587
- words: words.length > 0 ? words : undefined,
588
- };
589
- });
590
- // 判断是否为最终结果
591
- // specificFlags: 0b0010 (LAST_PACKET_NO_SEQ) 或 0b0011 (LAST_PACKET_WITH_SEQ) 表示最后一包
592
- const isFinal = specificFlags === MESSAGE_FLAGS.LAST_PACKET_NO_SEQ ||
593
- specificFlags === MESSAGE_FLAGS.LAST_PACKET_WITH_SEQ;
594
- return {
595
- text,
596
- isFinal,
597
- utterances,
598
- sequence: sequence ?? 0, // 如果没有 sequence,返回 0
599
- audioDuration,
600
- };
601
- }
602
- /**
603
- * 建立流式识别连接
604
- *
605
- * @description 建立 WebSocket 连接并发送初始配置请求
606
- *
607
- * @param params - 连接参数
608
- * @param callbacks - 事件回调
609
- * @returns 连接 ID
610
- */
611
- async connect(params, callbacks) {
612
- const { sessionId, audioFormat = 'pcm', sampleRate = 16000, channels = 1, enableSpeakerInfo = true,
613
- // 新增参数
614
- language, enableItn = true, enablePunc = true, enableDdc = true, enableNonstream = true, showUtterances = true, showSpeechRate = true, showVolume = true, enableLid = true, enableEmotionDetection = true, enableGenderDetection = true, resultType = 'full', enableAccelerateText = false, accelerateScore, vadSegmentDuration, endWindowSize = 800, forceToSpeechTime, sensitiveWordsFilter, corpus, } = params;
615
- // 生成连接 ID
616
- const connectionId = (0, uuid_1.v4)();
617
- // 验证配置
618
- if (!this.config.appId || !this.config.appAccessToken) {
619
- throw new Error('Volcengine config missing appId or appAccessToken');
620
- }
621
- if (!this.config.uid) {
622
- throw new Error('Volcengine config missing uid');
623
- }
624
- // 使用配置中的流式端点或默认端点
625
- const endpoint = this.config.endpoint;
626
- // 构建认证头
627
- const headers = this.buildHeaders(connectionId);
628
- this.logInfo('Creating WebSocket connection', {
629
- sessionId,
630
- connectionId,
631
- endpoint,
632
- audioFormat,
633
- });
634
- return new Promise((resolve, reject) => {
635
- // 创建 WebSocket 连接
636
- const ws = new ws_1.default(endpoint, { headers });
637
- // 初始化连接信息
638
- const connectionInfo = {
639
- ws,
640
- status: 'connecting',
641
- callbacks,
642
- sessionId,
643
- sequence: 0,
644
- transcript: '',
645
- utterances: [],
646
- connectParams: params,
647
- retryCount: 0,
648
- lastActivityTime: Date.now(),
649
- isReconnecting: false,
650
- pendingAudioBuffer: [],
651
- };
652
- this.connections.set(connectionId, connectionInfo);
653
- // 连接建立事件
654
- ws.on('open', async () => {
655
- try {
656
- this.logInfo('WebSocket connection opened', {
657
- connectionId,
658
- });
659
- // 发送初始请求(Full client request)
660
- const initRequest = this.buildInitRequest({
661
- audioFormat,
662
- sampleRate,
663
- channels,
664
- enableSpeakerInfo,
665
- language,
666
- enableItn,
667
- enablePunc,
668
- enableDdc,
669
- enableNonstream,
670
- showUtterances,
671
- showSpeechRate,
672
- showVolume,
673
- enableLid,
674
- enableEmotionDetection,
675
- enableGenderDetection,
676
- resultType,
677
- enableAccelerateText,
678
- accelerateScore,
679
- vadSegmentDuration,
680
- endWindowSize,
681
- forceToSpeechTime,
682
- sensitiveWordsFilter,
683
- corpus,
684
- });
685
- const message = await this.buildFullClientRequest(initRequest);
686
- ws.send(message);
687
- // 更新状态
688
- connectionInfo.status = 'connected';
689
- connectionInfo.retryCount = 0; // 重置重试计数
690
- connectionInfo.lastActivityTime = Date.now();
691
- // 启动心跳机制
692
- this.startHeartbeat(connectionId);
693
- callbacks.onConnected?.();
694
- resolve(connectionId);
695
- }
696
- catch (error) {
697
- this.logError('Failed to send init request', {
698
- connectionId,
699
- error: error.message,
700
- });
701
- connectionInfo.status = 'error';
702
- reject(error);
703
- }
704
- });
705
- // 消息接收事件
706
- ws.on('message', async (data) => {
707
- try {
708
- // 更新最后活动时间
709
- connectionInfo.lastActivityTime = Date.now();
710
- // 重置心跳超时
711
- this.resetHeartbeatTimeout(connectionId);
712
- // 记录接收到的消息详细信息(用于调试协议解析问题)
713
- if (data.length > 0) {
714
- const messageType = data.length >= 2 ? (data[1] >> 4) & 0x0f : -1;
715
- const specificFlags = data.length >= 2 ? data[1] & 0x0f : -1;
716
- const compression = data.length >= 3 ? data[2] & 0x0f : -1;
717
- this.logDebug('Received WebSocket message', {
718
- connectionId,
719
- messageSize: data.length,
720
- messageType,
721
- specificFlags,
722
- compression,
723
- firstBytes: data
724
- .slice(0, Math.min(20, data.length))
725
- .toString('hex'),
726
- });
727
- }
728
- const result = await this.parseServerResponse(data);
729
- // 更新累积结果
730
- if (result.text) {
731
- connectionInfo.transcript = result.text;
732
- }
733
- if (result.utterances && result.utterances.length > 0) {
734
- connectionInfo.utterances = result.utterances;
735
- }
736
- // 触发回调
737
- callbacks.onResult?.(result);
738
- // 如果是最终结果,更新状态
739
- if (result.isFinal) {
740
- connectionInfo.status = 'completed';
741
- // 完成后停止心跳
742
- this.stopHeartbeat(connectionId);
743
- }
744
- else {
745
- connectionInfo.status = 'streaming';
746
- }
747
- }
748
- catch (error) {
749
- this.logError('Failed to parse server response', {
750
- connectionId,
751
- error: error.message,
752
- });
753
- callbacks.onError?.(error);
754
- }
755
- });
756
- // 错误事件
757
- ws.on('error', (error) => {
758
- this.logError('WebSocket error', {
759
- connectionId,
760
- error: error.message,
761
- });
762
- connectionInfo.status = 'error';
763
- callbacks.onError?.(error);
764
- reject(error);
765
- });
766
- // 关闭事件
767
- ws.on('close', async (code, reason) => {
768
- this.logInfo('WebSocket connection closed', {
769
- connectionId,
770
- code,
771
- reason: reason.toString(),
772
- });
773
- // 停止心跳
774
- this.stopHeartbeat(connectionId);
775
- // 如果是异常关闭且未完成,尝试重连
776
- if (connectionInfo.status !== 'completed' &&
777
- connectionInfo.status !== 'disconnected' &&
778
- !connectionInfo.isReconnecting &&
779
- code !== 1000 // 正常关闭不重连
780
- ) {
781
- await this.attemptReconnect(connectionId);
782
- }
783
- else if (connectionInfo.status !== 'completed') {
784
- connectionInfo.status = 'disconnected';
785
- callbacks.onDisconnected?.();
786
- }
787
- });
788
- // 连接超时处理
789
- const timeout = setTimeout(() => {
790
- if (connectionInfo.status === 'connecting') {
791
- this.logError('WebSocket connection timeout', {
792
- connectionId,
793
- });
794
- ws.close();
795
- connectionInfo.status = 'error';
796
- reject(new Error('WebSocket connection timeout'));
797
- }
798
- }, 30000); // 30 秒超时
799
- // 连接成功后清除超时
800
- ws.on('open', () => {
801
- clearTimeout(timeout);
802
- });
803
- });
804
- }
805
- /**
806
- * 计算音频时长(毫秒)
807
- *
808
- * @description 根据音频数据大小和采样参数计算时长
809
- * @param audioBytes - 音频数据字节数
810
- * @param sampleRate - 采样率(默认 16000)
811
- * @param channels - 声道数(默认 1)
812
- * @param bitsPerSample - 采样位深(默认 16)
813
- * @returns 音频时长(毫秒)
814
- */
815
- calculateAudioDurationMs(audioBytes, sampleRate = 16000, channels = 1, bitsPerSample = 16) {
816
- const bytesPerSample = bitsPerSample / 8;
817
- const bytesPerSecond = sampleRate * channels * bytesPerSample;
818
- return (audioBytes / bytesPerSecond) * 1000;
819
- }
820
- /**
821
- * 发送音频数据
822
- *
823
- * @description 将音频数据分包发送到火山引擎服务
824
- * 文档建议单包音频大小 100-200ms,双向流式模式推荐 200ms
825
- *
826
- * @param connectionId - 连接 ID
827
- * @param audioData - 音频数据(Buffer)
828
- * @param isLast - 是否为最后一帧
829
- */
830
- async sendAudio(connectionId, audioData, isLast = false) {
831
- const connectionInfo = this.connections.get(connectionId);
832
- if (!connectionInfo) {
833
- throw new Error(`Connection not found: ${connectionId}`);
834
- }
835
- // 计算音频包时长并记录(仅对非空包进行检查)
836
- if (audioData.length > 0) {
837
- const { connectParams } = connectionInfo;
838
- const audioDurationMs = this.calculateAudioDurationMs(audioData.length, connectParams.sampleRate || 16000, connectParams.channels || 1);
839
- // 如果音频包时长超出推荐范围,记录调试日志
840
- if (audioDurationMs < RECOMMENDED_AUDIO_PACKET_MS.MIN ||
841
- audioDurationMs > RECOMMENDED_AUDIO_PACKET_MS.MAX * 2) {
842
- this.logDebug('Audio packet size outside recommended range', {
843
- connectionId,
844
- audioBytes: audioData.length,
845
- audioDurationMs: Math.round(audioDurationMs),
846
- recommendedMs: `${RECOMMENDED_AUDIO_PACKET_MS.MIN}-${RECOMMENDED_AUDIO_PACKET_MS.MAX}`,
847
- optimalMs: RECOMMENDED_AUDIO_PACKET_MS.OPTIMAL,
848
- });
849
- }
850
- }
851
- // 如果正在重连,将音频数据缓冲
852
- if (connectionInfo.isReconnecting) {
853
- this.bufferAudioDuringReconnect(connectionId, audioData);
854
- this.logInfo('Audio buffered during reconnect', {
855
- connectionId,
856
- bufferSize: connectionInfo.pendingAudioBuffer.length,
857
- });
858
- return;
859
- }
860
- // Allow resuming from 'completed' or 'disconnected' status
861
- // This handles the case where:
862
- // 1. The ASR provider marked the connection as completed due to receiving isFinal=true
863
- // 2. The WebSocket connection was disconnected (e.g., page refresh, network issue)
864
- // In both cases, we try to reactivate the connection
865
- if (connectionInfo.status !== 'connected' &&
866
- connectionInfo.status !== 'streaming' &&
867
- connectionInfo.status !== 'completed' &&
868
- connectionInfo.status !== 'disconnected') {
869
- throw new Error(`Invalid connection status: ${connectionInfo.status}`);
870
- }
871
- // If status is 'completed' or 'disconnected', try to reactivate
872
- if (connectionInfo.status === 'completed' ||
873
- connectionInfo.status === 'disconnected') {
874
- this.logInfo(`Reactivating ${connectionInfo.status} connection`, {
875
- connectionId,
876
- });
877
- // Check if WebSocket is still open
878
- if (connectionInfo.ws.readyState !== ws_1.default.OPEN) {
879
- // WebSocket is closed, need to reconnect
880
- this.logWarn('WebSocket closed, attempting reconnect', {
881
- connectionId,
882
- readyState: connectionInfo.ws.readyState,
883
- });
884
- // Try to reconnect
885
- try {
886
- await this.attemptReconnect(connectionId);
887
- // After reconnect, check if the connection is now active
888
- // Note: attemptReconnect updates connectionInfo in the Map
889
- const updatedInfo = this.connections.get(connectionId);
890
- if (!updatedInfo ||
891
- (updatedInfo.status !== 'connected' &&
892
- updatedInfo.status !== 'streaming')) {
893
- if (!isLast) {
894
- this.bufferAudioDuringReconnect(connectionId, audioData);
895
- return;
896
- }
897
- throw new Error('Failed to reconnect WebSocket');
898
- }
899
- // Reconnect succeeded, continue with the updated connection
900
- }
901
- catch (error) {
902
- // Reconnect failed
903
- if (!isLast) {
904
- this.bufferAudioDuringReconnect(connectionId, audioData);
905
- this.logError('Reconnect failed, audio buffered', {
906
- connectionId,
907
- error: error instanceof Error ? error.message : String(error),
908
- });
909
- return;
910
- }
911
- throw error;
912
- }
913
- }
914
- else {
915
- // WebSocket is still open, just update status
916
- connectionInfo.status = 'streaming';
917
- }
918
- }
919
- const { ws } = connectionInfo;
920
- if (ws.readyState !== ws_1.default.OPEN) {
921
- // WebSocket 未打开,尝试缓冲数据
922
- if (!isLast) {
923
- this.bufferAudioDuringReconnect(connectionId, audioData);
924
- this.logWarn('WebSocket not open, audio buffered', {
925
- connectionId,
926
- readyState: ws.readyState,
927
- });
928
- return;
929
- }
930
- throw new Error('WebSocket is not open');
931
- }
932
- // 构建并发送音频数据包
933
- const message = await this.buildAudioOnlyRequest(audioData, isLast);
934
- ws.send(message);
935
- // 更新状态和最后活动时间
936
- connectionInfo.status = 'streaming';
937
- connectionInfo.lastActivityTime = Date.now();
938
- if (isLast) {
939
- this.logInfo('Sent last audio packet', { connectionId });
940
- }
941
- }
942
- /**
943
- * 关闭连接
944
- *
945
- * @param connectionId - 连接 ID
946
- */
947
- async disconnect(connectionId) {
948
- const connectionInfo = this.connections.get(connectionId);
949
- if (!connectionInfo) {
950
- this.logWarn('Connection not found for disconnect', {
951
- connectionId,
952
- });
953
- return;
954
- }
955
- // 停止心跳
956
- this.stopHeartbeat(connectionId);
957
- // 标记为已断开,防止重连
958
- connectionInfo.status = 'disconnected';
959
- connectionInfo.isReconnecting = false;
960
- const { ws } = connectionInfo;
961
- if (ws.readyState === ws_1.default.OPEN) {
962
- ws.close(1000, 'Normal closure'); // 使用正常关闭码
963
- }
964
- this.connections.delete(connectionId);
965
- this.logInfo('Connection disconnected', { connectionId });
966
- }
967
- /**
968
- * 获取连接状态
969
- *
970
- * @param connectionId - 连接 ID
971
- * @returns 连接状态
972
- */
973
- getConnectionStatus(connectionId) {
974
- const connectionInfo = this.connections.get(connectionId);
975
- return connectionInfo?.status || 'disconnected';
976
- }
977
- /**
978
- * 获取累积的转写结果
979
- *
980
- * @param connectionId - 连接 ID
981
- * @returns 转写结果
982
- */
983
- getTranscript(connectionId) {
984
- const connectionInfo = this.connections.get(connectionId);
985
- if (!connectionInfo) {
986
- return null;
987
- }
988
- return {
989
- transcript: connectionInfo.transcript,
990
- utterances: connectionInfo.utterances,
991
- };
992
- }
993
- /**
994
- * 获取活跃连接数
995
- *
996
- * @returns 活跃连接数量
997
- */
998
- getActiveConnectionCount() {
999
- return this.connections.size;
1000
- }
1001
- /**
1002
- * 清理所有连接
1003
- *
1004
- * @description 关闭所有活跃的 WebSocket 连接
1005
- */
1006
- async cleanupAllConnections() {
1007
- const connectionIds = Array.from(this.connections.keys());
1008
- for (const connectionId of connectionIds) {
1009
- await this.disconnect(connectionId);
1010
- }
1011
- this.logInfo('All connections cleaned up');
1012
- }
1013
- // =========================================================================
1014
- // 心跳机制相关方法
1015
- // =========================================================================
1016
- /**
1017
- * 启动心跳机制
1018
- *
1019
- * @description 定期发送心跳包以保持连接活跃
1020
- * @param connectionId - 连接 ID
1021
- */
1022
- startHeartbeat(connectionId) {
1023
- const connectionInfo = this.connections.get(connectionId);
1024
- if (!connectionInfo)
1025
- return;
1026
- // 清除已有的心跳定时器
1027
- this.stopHeartbeat(connectionId);
1028
- // 设置新的心跳定时器
1029
- connectionInfo.heartbeatTimer = setInterval(() => {
1030
- this.sendHeartbeat(connectionId);
1031
- }, HEARTBEAT_CONFIG.interval);
1032
- this.logInfo('Heartbeat started', { connectionId });
1033
- }
1034
- /**
1035
- * 停止心跳机制
1036
- *
1037
- * @param connectionId - 连接 ID
1038
- */
1039
- stopHeartbeat(connectionId) {
1040
- const connectionInfo = this.connections.get(connectionId);
1041
- if (!connectionInfo)
1042
- return;
1043
- if (connectionInfo.heartbeatTimer) {
1044
- clearInterval(connectionInfo.heartbeatTimer);
1045
- connectionInfo.heartbeatTimer = undefined;
1046
- }
1047
- if (connectionInfo.heartbeatTimeoutTimer) {
1048
- clearTimeout(connectionInfo.heartbeatTimeoutTimer);
1049
- connectionInfo.heartbeatTimeoutTimer = undefined;
1050
- }
1051
- }
1052
- /**
1053
- * 发送心跳包
1054
- *
1055
- * @description 发送一个空的音频包作为心跳
1056
- * @param connectionId - 连接 ID
1057
- */
1058
- async sendHeartbeat(connectionId) {
1059
- const connectionInfo = this.connections.get(connectionId);
1060
- if (!connectionInfo)
1061
- return;
1062
- const { ws, status } = connectionInfo;
1063
- if (status !== 'connected' && status !== 'streaming') {
1064
- return;
1065
- }
1066
- if (ws.readyState !== ws_1.default.OPEN) {
1067
- this.logWarn('Cannot send heartbeat: WebSocket not open', {
1068
- connectionId,
1069
- readyState: ws.readyState,
1070
- });
1071
- return;
1072
- }
1073
- try {
1074
- // 发送空音频包作为心跳
1075
- const emptyAudio = Buffer.alloc(0);
1076
- const message = await this.buildAudioOnlyRequest(emptyAudio, false);
1077
- ws.send(message);
1078
- // 设置心跳超时检测
1079
- connectionInfo.heartbeatTimeoutTimer = setTimeout(() => {
1080
- this.handleHeartbeatTimeout(connectionId);
1081
- }, HEARTBEAT_CONFIG.timeout);
1082
- this.logInfo('Heartbeat sent', { connectionId });
1083
- }
1084
- catch (error) {
1085
- this.logError('Failed to send heartbeat', {
1086
- connectionId,
1087
- error: error.message,
1088
- });
1089
- }
1090
- }
1091
- /**
1092
- * 重置心跳超时
1093
- *
1094
- * @description 收到服务器响应时重置超时检测
1095
- * @param connectionId - 连接 ID
1096
- */
1097
- resetHeartbeatTimeout(connectionId) {
1098
- const connectionInfo = this.connections.get(connectionId);
1099
- if (!connectionInfo)
1100
- return;
1101
- if (connectionInfo.heartbeatTimeoutTimer) {
1102
- clearTimeout(connectionInfo.heartbeatTimeoutTimer);
1103
- connectionInfo.heartbeatTimeoutTimer = undefined;
1104
- }
1105
- }
1106
- /**
1107
- * 处理心跳超时
1108
- *
1109
- * @description 心跳超时表示连接可能已断开,触发重连
1110
- * @param connectionId - 连接 ID
1111
- */
1112
- handleHeartbeatTimeout(connectionId) {
1113
- const connectionInfo = this.connections.get(connectionId);
1114
- if (!connectionInfo)
1115
- return;
1116
- this.logWarn('Heartbeat timeout, connection may be dead', {
1117
- connectionId,
1118
- });
1119
- // 标记状态为错误
1120
- connectionInfo.status = 'error';
1121
- // 关闭当前连接并触发重连
1122
- const { ws } = connectionInfo;
1123
- if (ws.readyState === ws_1.default.OPEN) {
1124
- ws.close(4000, 'Heartbeat timeout');
1125
- }
1126
- }
1127
- // =========================================================================
1128
- // 重连机制相关方法
1129
- // =========================================================================
1130
- /**
1131
- * 尝试重新连接
1132
- *
1133
- * @description 使用指数退避策略进行重连
1134
- * @param connectionId - 连接 ID
1135
- */
1136
- async attemptReconnect(connectionId) {
1137
- const connectionInfo = this.connections.get(connectionId);
1138
- if (!connectionInfo)
1139
- return;
1140
- // 检查重试次数
1141
- if (connectionInfo.retryCount >= this.reconnectConfig.maxRetries) {
1142
- this.logError('Max reconnect attempts reached', {
1143
- connectionId,
1144
- retryCount: connectionInfo.retryCount,
1145
- });
1146
- connectionInfo.status = 'disconnected';
1147
- connectionInfo.callbacks.onError?.(new Error('Max reconnect attempts reached'));
1148
- connectionInfo.callbacks.onDisconnected?.();
1149
- return;
1150
- }
1151
- connectionInfo.isReconnecting = true;
1152
- connectionInfo.retryCount++;
1153
- // 计算延迟时间(指数退避)
1154
- const delay = Math.min(this.reconnectConfig.initialDelay *
1155
- Math.pow(this.reconnectConfig.backoffMultiplier, connectionInfo.retryCount - 1), this.reconnectConfig.maxDelay);
1156
- this.logInfo('Attempting reconnect', {
1157
- connectionId,
1158
- retryCount: connectionInfo.retryCount,
1159
- delayMs: delay,
1160
- });
1161
- // 等待延迟后重连
1162
- await new Promise((resolve) => setTimeout(resolve, delay));
1163
- // 检查是否仍需要重连
1164
- if (connectionInfo.status === 'completed' ||
1165
- connectionInfo.status === 'disconnected') {
1166
- this.logInfo('Reconnect cancelled: status changed', {
1167
- connectionId,
1168
- status: connectionInfo.status,
1169
- });
1170
- return;
1171
- }
1172
- try {
1173
- // 创建新的 WebSocket 连接
1174
- await this.reconnect(connectionId);
1175
- }
1176
- catch (error) {
1177
- this.logError('Reconnect failed', {
1178
- connectionId,
1179
- error: error.message,
1180
- });
1181
- // 递归重试
1182
- await this.attemptReconnect(connectionId);
1183
- }
1184
- }
1185
- /**
1186
- * 执行重连
1187
- *
1188
- * @description 创建新的 WebSocket 连接并恢复状态
1189
- * @param connectionId - 连接 ID
1190
- */
1191
- async reconnect(connectionId) {
1192
- const connectionInfo = this.connections.get(connectionId);
1193
- if (!connectionInfo) {
1194
- throw new Error('Connection info not found');
1195
- }
1196
- const { connectParams, callbacks } = connectionInfo;
1197
- // 使用配置中的流式端点或默认端点
1198
- const endpoint = this.config.endpoint;
1199
- const headers = this.buildHeaders(connectionId);
1200
- return new Promise((resolve, reject) => {
1201
- const ws = new ws_1.default(endpoint, { headers });
1202
- // 连接建立事件
1203
- ws.on('open', async () => {
1204
- try {
1205
- this.logInfo('Reconnected successfully', { connectionId });
1206
- // 发送初始请求(使用保存的连接参数)
1207
- const initRequest = this.buildInitRequest({
1208
- audioFormat: connectParams.audioFormat || 'pcm',
1209
- sampleRate: connectParams.sampleRate || 16000,
1210
- channels: connectParams.channels || 1,
1211
- enableSpeakerInfo: connectParams.enableSpeakerInfo !== false,
1212
- language: connectParams.language,
1213
- enableItn: connectParams.enableItn ?? true,
1214
- enablePunc: connectParams.enablePunc ?? true,
1215
- enableDdc: connectParams.enableDdc ?? true,
1216
- enableNonstream: connectParams.enableNonstream ?? true,
1217
- showUtterances: connectParams.showUtterances ?? true,
1218
- showSpeechRate: connectParams.showSpeechRate ?? true,
1219
- showVolume: connectParams.showVolume ?? true,
1220
- enableLid: connectParams.enableLid ?? true,
1221
- enableEmotionDetection: connectParams.enableEmotionDetection ?? true,
1222
- enableGenderDetection: connectParams.enableGenderDetection ?? true,
1223
- resultType: connectParams.resultType || 'full',
1224
- enableAccelerateText: connectParams.enableAccelerateText ?? false,
1225
- accelerateScore: connectParams.accelerateScore,
1226
- vadSegmentDuration: connectParams.vadSegmentDuration,
1227
- endWindowSize: connectParams.endWindowSize ?? 800,
1228
- forceToSpeechTime: connectParams.forceToSpeechTime,
1229
- sensitiveWordsFilter: connectParams.sensitiveWordsFilter,
1230
- corpus: connectParams.corpus,
1231
- });
1232
- const message = await this.buildFullClientRequest(initRequest);
1233
- ws.send(message);
1234
- // 更新连接信息
1235
- connectionInfo.ws = ws;
1236
- connectionInfo.status = 'connected';
1237
- connectionInfo.isReconnecting = false;
1238
- connectionInfo.lastActivityTime = Date.now();
1239
- // 重新启动心跳
1240
- this.startHeartbeat(connectionId);
1241
- // 发送缓冲的音频数据
1242
- if (connectionInfo.pendingAudioBuffer.length > 0) {
1243
- this.logInfo('Sending buffered audio data', {
1244
- connectionId,
1245
- bufferCount: connectionInfo.pendingAudioBuffer.length,
1246
- });
1247
- for (const buffer of connectionInfo.pendingAudioBuffer) {
1248
- await this.sendAudio(connectionId, buffer, false);
1249
- }
1250
- connectionInfo.pendingAudioBuffer = [];
1251
- }
1252
- callbacks.onConnected?.();
1253
- resolve();
1254
- }
1255
- catch (error) {
1256
- this.logError('Failed to initialize reconnected session', {
1257
- connectionId,
1258
- error: error.message,
1259
- });
1260
- reject(error);
1261
- }
1262
- });
1263
- // 消息接收事件
1264
- ws.on('message', async (data) => {
1265
- try {
1266
- connectionInfo.lastActivityTime = Date.now();
1267
- this.resetHeartbeatTimeout(connectionId);
1268
- const result = await this.parseServerResponse(data);
1269
- if (result.text) {
1270
- connectionInfo.transcript = result.text;
1271
- }
1272
- if (result.utterances && result.utterances.length > 0) {
1273
- connectionInfo.utterances = result.utterances;
1274
- }
1275
- callbacks.onResult?.(result);
1276
- if (result.isFinal) {
1277
- connectionInfo.status = 'completed';
1278
- this.stopHeartbeat(connectionId);
1279
- }
1280
- else {
1281
- connectionInfo.status = 'streaming';
1282
- }
1283
- }
1284
- catch (error) {
1285
- this.logError('Failed to parse server response', {
1286
- connectionId,
1287
- error: error.message,
1288
- });
1289
- callbacks.onError?.(error);
1290
- }
1291
- });
1292
- // 错误事件
1293
- ws.on('error', (error) => {
1294
- this.logError('Reconnect WebSocket error', {
1295
- connectionId,
1296
- error: error.message,
1297
- });
1298
- reject(error);
1299
- });
1300
- // 关闭事件
1301
- ws.on('close', async (code, reason) => {
1302
- this.logInfo('Reconnected WebSocket closed', {
1303
- connectionId,
1304
- code,
1305
- reason: reason.toString(),
1306
- });
1307
- this.stopHeartbeat(connectionId);
1308
- if (connectionInfo.status !== 'completed' &&
1309
- connectionInfo.status !== 'disconnected' &&
1310
- code !== 1000) {
1311
- await this.attemptReconnect(connectionId);
1312
- }
1313
- });
1314
- // 连接超时
1315
- const timeout = setTimeout(() => {
1316
- this.logError('Reconnect timeout', { connectionId });
1317
- ws.close();
1318
- reject(new Error('Reconnect timeout'));
1319
- }, 15000);
1320
- ws.on('open', () => {
1321
- clearTimeout(timeout);
1322
- });
1323
- });
1324
- }
1325
- /**
1326
- * 缓冲音频数据(用于重连期间)
1327
- *
1328
- * @description 在重连期间缓冲音频数据,重连成功后发送
1329
- * @param connectionId - 连接 ID
1330
- * @param audioData - 音频数据
1331
- */
1332
- bufferAudioDuringReconnect(connectionId, audioData) {
1333
- const connectionInfo = this.connections.get(connectionId);
1334
- if (!connectionInfo)
1335
- return;
1336
- // 限制缓冲区大小(最多 100 个包)
1337
- if (connectionInfo.pendingAudioBuffer.length < 100) {
1338
- connectionInfo.pendingAudioBuffer.push(audioData);
1339
- }
1340
- else {
1341
- this.logWarn('Audio buffer full during reconnect', {
1342
- connectionId,
1343
- });
1344
- }
1345
- }
1346
- /**
1347
- * 检查连接是否正在重连
1348
- *
1349
- * @param connectionId - 连接 ID
1350
- * @returns 是否正在重连
1351
- */
1352
- isReconnecting(connectionId) {
1353
- const connectionInfo = this.connections.get(connectionId);
1354
- return connectionInfo?.isReconnecting ?? false;
1355
- }
1356
- }
1357
- exports.VolcengineStreamingAsrProvider = VolcengineStreamingAsrProvider;
1358
- //# sourceMappingURL=volcengine-streaming.provider.js.map