@dev-anywhere/relay 0.3.14 → 0.4.2
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/{chunk-DFVUNUQH.js → chunk-ERH2EO6I.js} +1612 -153
- package/dist/chunk-ERH2EO6I.js.map +1 -0
- package/dist/handlers/client.d.ts +3 -1
- package/dist/handlers/client.d.ts.map +1 -1
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime-env.d.ts +6 -0
- package/dist/runtime-env.d.ts.map +1 -1
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/voice/asr-ws.d.ts +8 -0
- package/dist/voice/asr-ws.d.ts.map +1 -0
- package/dist/voice/bailian-asr.d.ts +35 -0
- package/dist/voice/bailian-asr.d.ts.map +1 -0
- package/dist/voice/bailian-endpoints.d.ts +4 -0
- package/dist/voice/bailian-endpoints.d.ts.map +1 -0
- package/dist/voice/bailian-provider.d.ts +13 -0
- package/dist/voice/bailian-provider.d.ts.map +1 -0
- package/dist/voice/bailian-tts.d.ts +33 -0
- package/dist/voice/bailian-tts.d.ts.map +1 -0
- package/dist/voice/capabilities.d.ts +21 -0
- package/dist/voice/capabilities.d.ts.map +1 -0
- package/dist/voice/client-controls.d.ts +7 -0
- package/dist/voice/client-controls.d.ts.map +1 -0
- package/dist/voice/config-store.d.ts +22 -0
- package/dist/voice/config-store.d.ts.map +1 -0
- package/dist/voice/config-test.d.ts +22 -0
- package/dist/voice/config-test.d.ts.map +1 -0
- package/dist/voice/provider.d.ts +41 -0
- package/dist/voice/provider.d.ts.map +1 -0
- package/dist/voice/tts-ws.d.ts +8 -0
- package/dist/voice/tts-ws.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-DFVUNUQH.js.map +0 -1
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/server.ts
|
|
4
4
|
import express from "express";
|
|
5
|
-
import { existsSync } from "fs";
|
|
5
|
+
import { existsSync as existsSync2 } from "fs";
|
|
6
6
|
import { createServer } from "http";
|
|
7
7
|
import { homedir } from "os";
|
|
8
|
-
import { dirname as
|
|
8
|
+
import { dirname as dirname3, resolve } from "path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
11
11
|
|
|
@@ -262,13 +262,213 @@ var MessageEnvelopeSchema = z6.discriminatedUnion("type", [
|
|
|
262
262
|
})
|
|
263
263
|
]);
|
|
264
264
|
|
|
265
|
+
// ../../packages/shared/dist/schemas/voice.js
|
|
266
|
+
import { z as z7 } from "zod";
|
|
267
|
+
var voiceProviderValues = ["aliyun-bailian"];
|
|
268
|
+
var voiceRegionValues = ["cn", "intl"];
|
|
269
|
+
var voiceOptionSourceValues = ["official", "custom"];
|
|
270
|
+
var voiceOptionGenderValues = ["male", "female", "unknown"];
|
|
271
|
+
var VoiceProviderConfigSchema = z7.object({
|
|
272
|
+
provider: z7.enum(voiceProviderValues),
|
|
273
|
+
configured: z7.boolean(),
|
|
274
|
+
region: z7.enum(voiceRegionValues),
|
|
275
|
+
asrModel: z7.string().min(1),
|
|
276
|
+
ttsModel: z7.string().min(1),
|
|
277
|
+
ttsVoice: z7.string().min(1),
|
|
278
|
+
turnIdleSeconds: z7.number().int().positive().safe().default(3)
|
|
279
|
+
}).strict();
|
|
280
|
+
var VoiceConfigUpdateSchema = z7.object({
|
|
281
|
+
provider: z7.enum(voiceProviderValues).optional(),
|
|
282
|
+
apiKey: z7.string().min(1).optional(),
|
|
283
|
+
clearApiKey: z7.boolean().optional(),
|
|
284
|
+
region: z7.enum(voiceRegionValues).optional(),
|
|
285
|
+
asrModel: z7.string().min(1).optional(),
|
|
286
|
+
ttsModel: z7.string().min(1).optional(),
|
|
287
|
+
ttsVoice: z7.string().min(1).optional(),
|
|
288
|
+
turnIdleSeconds: z7.number().int().positive().safe().optional()
|
|
289
|
+
}).strict();
|
|
290
|
+
var VoiceOptionSchema = z7.object({
|
|
291
|
+
value: z7.string().min(1),
|
|
292
|
+
label: z7.string().min(1),
|
|
293
|
+
description: z7.string().min(1).optional(),
|
|
294
|
+
gender: z7.enum(voiceOptionGenderValues).optional(),
|
|
295
|
+
age: z7.string().min(1).optional(),
|
|
296
|
+
model: z7.string().min(1).optional(),
|
|
297
|
+
source: z7.enum(voiceOptionSourceValues)
|
|
298
|
+
}).strict();
|
|
299
|
+
var VoiceCapabilitiesSchema = z7.object({
|
|
300
|
+
asrModels: z7.array(VoiceOptionSchema),
|
|
301
|
+
ttsModels: z7.array(VoiceOptionSchema),
|
|
302
|
+
ttsVoices: z7.array(VoiceOptionSchema),
|
|
303
|
+
fetchedAt: z7.number().optional()
|
|
304
|
+
}).strict();
|
|
305
|
+
var BUNDLED_BAILIAN_ASR_MODELS = [
|
|
306
|
+
{
|
|
307
|
+
value: "qwen3-asr-flash-realtime",
|
|
308
|
+
label: "Qwen3 ASR Flash Realtime",
|
|
309
|
+
source: "official"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
value: "qwen3-asr-flash-realtime-2026-02-10",
|
|
313
|
+
label: "Qwen3 ASR Flash Realtime \xB7 2026-02-10",
|
|
314
|
+
source: "official"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
value: "qwen3-asr-flash-realtime-2025-10-27",
|
|
318
|
+
label: "Qwen3 ASR Flash Realtime \xB7 2025-10-27",
|
|
319
|
+
source: "official"
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
var BUNDLED_BAILIAN_TTS_MODELS = [
|
|
323
|
+
{
|
|
324
|
+
value: "cosyvoice-v3-flash",
|
|
325
|
+
label: "CosyVoice V3 Flash \xB7 \u7CFB\u7EDF\u97F3\u8272",
|
|
326
|
+
source: "official"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
value: "cosyvoice-v3-plus",
|
|
330
|
+
label: "CosyVoice V3 Plus \xB7 \u7CFB\u7EDF\u97F3\u8272",
|
|
331
|
+
source: "official"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
value: "cosyvoice-v3.5-flash",
|
|
335
|
+
label: "CosyVoice V3.5 Flash \xB7 \u81EA\u5B9A\u4E49\u97F3\u8272",
|
|
336
|
+
source: "official"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
value: "cosyvoice-v3.5-plus",
|
|
340
|
+
label: "CosyVoice V3.5 Plus \xB7 \u81EA\u5B9A\u4E49\u97F3\u8272",
|
|
341
|
+
source: "official"
|
|
342
|
+
}
|
|
343
|
+
];
|
|
344
|
+
var BUNDLED_BAILIAN_TTS_VOICES = [
|
|
345
|
+
{
|
|
346
|
+
value: "longanyang",
|
|
347
|
+
label: "\u9F99\u5B89\u6D0B \xB7 \u7537 \xB7 \u9633\u5149\u5927\u7537\u5B69 \xB7 \u5E74\u9F84 20-30",
|
|
348
|
+
gender: "male",
|
|
349
|
+
age: "20-30",
|
|
350
|
+
model: "cosyvoice-v3-flash",
|
|
351
|
+
source: "official"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
value: "longanhuan",
|
|
355
|
+
label: "\u9F99\u5B89\u6B22 \xB7 \u5973 \xB7 \u6B22\u8131\u5143\u6C14 \xB7 \u5E74\u9F84 20-30",
|
|
356
|
+
gender: "female",
|
|
357
|
+
age: "20-30",
|
|
358
|
+
model: "cosyvoice-v3-flash",
|
|
359
|
+
source: "official"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
value: "longhuhu_v3",
|
|
363
|
+
label: "\u9F99\u547C\u547C \xB7 \u5973 \xB7 \u5929\u771F\u70C2\u6F2B\u5973\u7AE5 \xB7 \u5E74\u9F84 6-10",
|
|
364
|
+
gender: "female",
|
|
365
|
+
age: "6-10",
|
|
366
|
+
model: "cosyvoice-v3-flash",
|
|
367
|
+
source: "official"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
value: "longpaopao_v3",
|
|
371
|
+
label: "\u9F99\u6CE1\u6CE1 \xB7 \u672A\u77E5 \xB7 \u98DE\u5929\u6CE1\u6CE1\u97F3 \xB7 \u5E74\u9F84 6-15",
|
|
372
|
+
gender: "unknown",
|
|
373
|
+
age: "6-15",
|
|
374
|
+
model: "cosyvoice-v3-flash",
|
|
375
|
+
source: "official"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
value: "longjielidou_v3",
|
|
379
|
+
label: "\u9F99\u6770\u529B\u8C46 \xB7 \u7537 \xB7 \u9633\u5149\u987D\u76AE \xB7 \u5E74\u9F84 10",
|
|
380
|
+
gender: "male",
|
|
381
|
+
age: "10",
|
|
382
|
+
model: "cosyvoice-v3-flash",
|
|
383
|
+
source: "official"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
value: "longxian_v3",
|
|
387
|
+
label: "\u9F99\u4ED9 \xB7 \u5973 \xB7 \u8C6A\u653E\u53EF\u7231 \xB7 \u5E74\u9F84 12",
|
|
388
|
+
gender: "female",
|
|
389
|
+
age: "12",
|
|
390
|
+
model: "cosyvoice-v3-flash",
|
|
391
|
+
source: "official"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
value: "longling_v3",
|
|
395
|
+
label: "\u9F99\u94C3 \xB7 \u5973 \xB7 \u7A1A\u6C14\u5446\u677F \xB7 \u5E74\u9F84 10",
|
|
396
|
+
gender: "female",
|
|
397
|
+
age: "10",
|
|
398
|
+
model: "cosyvoice-v3-flash",
|
|
399
|
+
source: "official"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
value: "longjiaxin_v3",
|
|
403
|
+
label: "\u9F99\u5609\u6B23 \xB7 \u5973 \xB7 \u4F18\u96C5\u7CA4\u8BED \xB7 \u5E74\u9F84 30-35",
|
|
404
|
+
gender: "female",
|
|
405
|
+
age: "30-35",
|
|
406
|
+
model: "cosyvoice-v3-flash",
|
|
407
|
+
source: "official"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
value: "longanyue_v3",
|
|
411
|
+
label: "\u9F99\u5B89\u7CA4 \xB7 \u7537 \xB7 \u6B22\u8131\u7CA4\u8BED \xB7 \u5E74\u9F84 25-35",
|
|
412
|
+
gender: "male",
|
|
413
|
+
age: "25-35",
|
|
414
|
+
model: "cosyvoice-v3-flash",
|
|
415
|
+
source: "official"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
value: "longlaotie_v3",
|
|
419
|
+
label: "\u9F99\u8001\u94C1 \xB7 \u7537 \xB7 \u4E1C\u5317\u76F4\u7387 \xB7 \u5E74\u9F84 25-30",
|
|
420
|
+
gender: "male",
|
|
421
|
+
age: "25-30",
|
|
422
|
+
model: "cosyvoice-v3-flash",
|
|
423
|
+
source: "official"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
value: "longanyang",
|
|
427
|
+
label: "\u9F99\u5B89\u6D0B \xB7 \u7537 \xB7 \u9633\u5149\u5927\u7537\u5B69 \xB7 \u5E74\u9F84 20-30",
|
|
428
|
+
gender: "male",
|
|
429
|
+
age: "20-30",
|
|
430
|
+
model: "cosyvoice-v3-plus",
|
|
431
|
+
source: "official"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
value: "longanhuan",
|
|
435
|
+
label: "\u9F99\u5B89\u6B22 \xB7 \u5973 \xB7 \u6B22\u8131\u5143\u6C14 \xB7 \u5E74\u9F84 20-30",
|
|
436
|
+
gender: "female",
|
|
437
|
+
age: "20-30",
|
|
438
|
+
model: "cosyvoice-v3-plus",
|
|
439
|
+
source: "official"
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
function cloneVoiceOption(option) {
|
|
443
|
+
return { ...option };
|
|
444
|
+
}
|
|
445
|
+
function createBundledBailianVoiceCapabilities(fetchedAt) {
|
|
446
|
+
return {
|
|
447
|
+
asrModels: BUNDLED_BAILIAN_ASR_MODELS.map(cloneVoiceOption),
|
|
448
|
+
ttsModels: BUNDLED_BAILIAN_TTS_MODELS.map(cloneVoiceOption),
|
|
449
|
+
ttsVoices: BUNDLED_BAILIAN_TTS_VOICES.map(cloneVoiceOption),
|
|
450
|
+
...typeof fetchedAt === "number" ? { fetchedAt } : {}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
var VoiceSummaryReasonSchema = z7.enum([
|
|
454
|
+
"code",
|
|
455
|
+
"table",
|
|
456
|
+
"diff",
|
|
457
|
+
"log",
|
|
458
|
+
"stack_trace",
|
|
459
|
+
"long_list",
|
|
460
|
+
"long_text",
|
|
461
|
+
"mixed",
|
|
462
|
+
"approval"
|
|
463
|
+
]);
|
|
464
|
+
|
|
265
465
|
// ../../packages/shared/dist/builders/index.js
|
|
266
466
|
function serializeControl(msg) {
|
|
267
467
|
return JSON.stringify(msg);
|
|
268
468
|
}
|
|
269
469
|
|
|
270
470
|
// ../../packages/shared/dist/schemas/relay-control.js
|
|
271
|
-
import { z as
|
|
471
|
+
import { z as z8 } from "zod";
|
|
272
472
|
|
|
273
473
|
// ../../packages/shared/dist/constants/relay-errors.js
|
|
274
474
|
var RelayErrorCode = {
|
|
@@ -295,59 +495,59 @@ var ControlErrorCode = {
|
|
|
295
495
|
};
|
|
296
496
|
|
|
297
497
|
// ../../packages/shared/dist/schemas/relay-control.js
|
|
298
|
-
var ProxyInfoSchema =
|
|
498
|
+
var ProxyInfoSchema = z8.object({
|
|
299
499
|
proxyId: IdSchema,
|
|
300
|
-
name:
|
|
301
|
-
online:
|
|
302
|
-
sessions:
|
|
500
|
+
name: z8.string().optional(),
|
|
501
|
+
online: z8.boolean(),
|
|
502
|
+
sessions: z8.array(z8.string()).optional()
|
|
303
503
|
});
|
|
304
|
-
var AgentCliAvailabilitySchema =
|
|
305
|
-
available:
|
|
306
|
-
command:
|
|
307
|
-
error:
|
|
308
|
-
suggestions:
|
|
504
|
+
var AgentCliAvailabilitySchema = z8.object({
|
|
505
|
+
available: z8.boolean(),
|
|
506
|
+
command: z8.string().optional(),
|
|
507
|
+
error: z8.string().optional(),
|
|
508
|
+
suggestions: z8.array(z8.string()).optional()
|
|
309
509
|
});
|
|
310
|
-
var AgentCliStatusSchema =
|
|
510
|
+
var AgentCliStatusSchema = z8.object({
|
|
311
511
|
claude: AgentCliAvailabilitySchema,
|
|
312
512
|
codex: AgentCliAvailabilitySchema
|
|
313
513
|
});
|
|
314
|
-
var DirEntrySchema =
|
|
315
|
-
var FileTreeGroupSchema =
|
|
316
|
-
path:
|
|
317
|
-
entries:
|
|
514
|
+
var DirEntrySchema = z8.object({ name: z8.string(), isDir: z8.boolean() });
|
|
515
|
+
var FileTreeGroupSchema = z8.object({
|
|
516
|
+
path: z8.string(),
|
|
517
|
+
entries: z8.array(DirEntrySchema)
|
|
318
518
|
});
|
|
319
|
-
var CommandEntrySchema =
|
|
320
|
-
name:
|
|
321
|
-
description:
|
|
322
|
-
argumentHint:
|
|
323
|
-
source:
|
|
519
|
+
var CommandEntrySchema = z8.object({
|
|
520
|
+
name: z8.string(),
|
|
521
|
+
description: z8.string(),
|
|
522
|
+
argumentHint: z8.string().optional(),
|
|
523
|
+
source: z8.string()
|
|
324
524
|
});
|
|
325
|
-
var HistorySessionSchema =
|
|
326
|
-
id:
|
|
327
|
-
title:
|
|
328
|
-
projectDir:
|
|
329
|
-
updatedAt:
|
|
330
|
-
provider:
|
|
525
|
+
var HistorySessionSchema = z8.object({
|
|
526
|
+
id: z8.string(),
|
|
527
|
+
title: z8.string(),
|
|
528
|
+
projectDir: z8.string(),
|
|
529
|
+
updatedAt: z8.number(),
|
|
530
|
+
provider: z8.enum(providerValues).optional()
|
|
331
531
|
});
|
|
332
|
-
var SessionHistoryMessageSchema =
|
|
333
|
-
role:
|
|
334
|
-
text:
|
|
335
|
-
timestamp:
|
|
336
|
-
cursor:
|
|
532
|
+
var SessionHistoryMessageSchema = z8.object({
|
|
533
|
+
role: z8.enum(["user", "assistant"]),
|
|
534
|
+
text: z8.string(),
|
|
535
|
+
timestamp: z8.number().optional(),
|
|
536
|
+
cursor: z8.string().optional()
|
|
337
537
|
});
|
|
338
538
|
var RequestIdShape = { requestId: IdSchema.optional() };
|
|
339
|
-
var ControlErrorCodeSchema =
|
|
539
|
+
var ControlErrorCodeSchema = z8.enum(Object.values(ControlErrorCode));
|
|
340
540
|
var RequestErrorShape = {
|
|
341
|
-
error:
|
|
541
|
+
error: z8.string().optional(),
|
|
342
542
|
errorCode: ControlErrorCodeSchema.optional()
|
|
343
543
|
};
|
|
344
|
-
var ClipboardImageMimeTypeSchema =
|
|
544
|
+
var ClipboardImageMimeTypeSchema = z8.enum(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
345
545
|
function control(type, shape, directions) {
|
|
346
546
|
return {
|
|
347
547
|
type,
|
|
348
548
|
directions: new Set(Array.isArray(directions) ? directions : directions ? [directions] : []),
|
|
349
|
-
schema:
|
|
350
|
-
type:
|
|
549
|
+
schema: z8.object({
|
|
550
|
+
type: z8.literal(type),
|
|
351
551
|
...shape ?? {}
|
|
352
552
|
})
|
|
353
553
|
};
|
|
@@ -355,36 +555,75 @@ function control(type, shape, directions) {
|
|
|
355
555
|
var relayControlDefinitions = [
|
|
356
556
|
control("proxy_register", {
|
|
357
557
|
proxyId: IdSchema,
|
|
358
|
-
name:
|
|
558
|
+
name: z8.string().optional()
|
|
359
559
|
}),
|
|
360
560
|
control("proxy_register_response", {
|
|
361
|
-
status:
|
|
561
|
+
status: z8.enum(["new", "reconnected"])
|
|
362
562
|
}),
|
|
363
563
|
control("proxy_list_request", RequestIdShape),
|
|
364
564
|
control("proxy_list_response", {
|
|
365
565
|
...RequestIdShape,
|
|
366
|
-
proxies:
|
|
566
|
+
proxies: z8.array(ProxyInfoSchema)
|
|
367
567
|
}),
|
|
368
568
|
control("proxy_select", { ...RequestIdShape, proxyId: IdSchema }),
|
|
369
569
|
control("proxy_select_response", {
|
|
370
570
|
...RequestIdShape,
|
|
371
|
-
success:
|
|
571
|
+
success: z8.boolean(),
|
|
372
572
|
proxyId: IdSchema.optional(),
|
|
373
573
|
...RequestErrorShape
|
|
374
574
|
}),
|
|
375
575
|
control("relay_error", {
|
|
376
|
-
code:
|
|
377
|
-
message:
|
|
576
|
+
code: z8.enum(Object.values(RelayErrorCode)),
|
|
577
|
+
message: z8.string(),
|
|
378
578
|
// 可选 requestId: relay 把 client 发来 raw 的 requestId 字段透传回来,
|
|
379
579
|
// client 侧 waitForMessage 据此把对应 pending request 立即拒掉而不必等到 timeout。
|
|
380
580
|
requestId: IdSchema.optional()
|
|
381
581
|
}),
|
|
582
|
+
// Voice Pilot config is relay-local: client reads/updates the relay's stored provider settings.
|
|
583
|
+
control("voice_config_request", RequestIdShape),
|
|
584
|
+
control("voice_config_response", {
|
|
585
|
+
...RequestIdShape,
|
|
586
|
+
...RequestErrorShape,
|
|
587
|
+
config: VoiceProviderConfigSchema.optional()
|
|
588
|
+
}),
|
|
589
|
+
control("voice_config_update", {
|
|
590
|
+
...RequestIdShape,
|
|
591
|
+
config: VoiceConfigUpdateSchema
|
|
592
|
+
}),
|
|
593
|
+
control("voice_config_update_response", {
|
|
594
|
+
...RequestIdShape,
|
|
595
|
+
...RequestErrorShape,
|
|
596
|
+
success: z8.boolean(),
|
|
597
|
+
config: VoiceProviderConfigSchema.optional()
|
|
598
|
+
}),
|
|
599
|
+
control("voice_config_test", {
|
|
600
|
+
...RequestIdShape,
|
|
601
|
+
config: VoiceConfigUpdateSchema.optional()
|
|
602
|
+
}),
|
|
603
|
+
control("voice_config_test_response", {
|
|
604
|
+
...RequestIdShape,
|
|
605
|
+
...RequestErrorShape,
|
|
606
|
+
success: z8.boolean(),
|
|
607
|
+
audioBase64: z8.string().optional(),
|
|
608
|
+
audioSampleRate: z8.number().int().positive().optional(),
|
|
609
|
+
audioEncoding: z8.literal("pcm_s16le").optional(),
|
|
610
|
+
transcript: z8.string().optional()
|
|
611
|
+
}),
|
|
612
|
+
control("voice_capabilities_request", {
|
|
613
|
+
...RequestIdShape,
|
|
614
|
+
region: z8.enum(voiceRegionValues).optional()
|
|
615
|
+
}),
|
|
616
|
+
control("voice_capabilities_response", {
|
|
617
|
+
...RequestIdShape,
|
|
618
|
+
...RequestErrorShape,
|
|
619
|
+
capabilities: VoiceCapabilitiesSchema.optional()
|
|
620
|
+
}),
|
|
382
621
|
// 客户端注册协议
|
|
383
622
|
control("client_register", {
|
|
384
623
|
clientId: IdSchema
|
|
385
624
|
}),
|
|
386
625
|
control("client_register_response", {
|
|
387
|
-
status:
|
|
626
|
+
status: z8.enum(["restored", "proxy_offline", "new"]),
|
|
388
627
|
proxyId: IdSchema.optional()
|
|
389
628
|
}),
|
|
390
629
|
// Proxy 离线通知
|
|
@@ -403,51 +642,51 @@ var relayControlDefinitions = [
|
|
|
403
642
|
control("dir_list_request", {
|
|
404
643
|
proxyId: IdSchema.optional(),
|
|
405
644
|
...RequestIdShape,
|
|
406
|
-
path:
|
|
645
|
+
path: z8.string()
|
|
407
646
|
}, "client_to_proxy"),
|
|
408
|
-
control("dir_list_response", { ...RequestIdShape, ...RequestErrorShape, entries:
|
|
647
|
+
control("dir_list_response", { ...RequestIdShape, ...RequestErrorShape, entries: z8.array(DirEntrySchema), path: z8.string() }, "proxy_to_client"),
|
|
409
648
|
// 目录创建请求与响应
|
|
410
|
-
control("dir_create_request", { ...RequestIdShape, path:
|
|
649
|
+
control("dir_create_request", { ...RequestIdShape, path: z8.string() }, "client_to_proxy"),
|
|
411
650
|
control("dir_create_response", {
|
|
412
651
|
...RequestIdShape,
|
|
413
652
|
...RequestErrorShape,
|
|
414
|
-
path:
|
|
415
|
-
success:
|
|
653
|
+
path: z8.string(),
|
|
654
|
+
success: z8.boolean()
|
|
416
655
|
}, "proxy_to_client"),
|
|
417
656
|
// 命令列表推送,proxy 将可用命令列表推给 client
|
|
418
|
-
control("command_list_push", { commands:
|
|
657
|
+
control("command_list_push", { commands: z8.array(CommandEntrySchema) }, "proxy_to_client"),
|
|
419
658
|
// 文件树推送: 按目录分组, 首组 path 即为 session cwd
|
|
420
659
|
// 前端直接把每组写入 tree[path], 与 dir_list_response 共享 cache slot
|
|
421
660
|
control("file_tree_push", {
|
|
422
|
-
groups:
|
|
661
|
+
groups: z8.array(FileTreeGroupSchema)
|
|
423
662
|
}, "proxy_to_client"),
|
|
424
663
|
// 会话列表请求与权限模式变更
|
|
425
664
|
control("session_list", void 0, ["client_to_proxy", "proxy_to_client"]),
|
|
426
665
|
control("permission_mode_change", {
|
|
427
|
-
mode:
|
|
666
|
+
mode: z8.enum(["default", "auto_accept", "plan"]),
|
|
428
667
|
// sessionId 可选:传入时 proxy 按该会话的 mode 分叉(PTY 发 Tab ANSI),未传走全局日志行为
|
|
429
668
|
sessionId: IdSchema.optional()
|
|
430
669
|
}, "client_to_proxy"),
|
|
431
670
|
// 会话历史浏览
|
|
432
671
|
control("session_history_request", RequestIdShape, "client_to_proxy"),
|
|
433
|
-
control("session_history_response", { ...RequestIdShape, sessions:
|
|
672
|
+
control("session_history_response", { ...RequestIdShape, sessions: z8.array(HistorySessionSchema) }, "proxy_to_client"),
|
|
434
673
|
// PTY 语义状态,从 Envelope 迁移到 Control 层
|
|
435
674
|
control("pty_state", { sessionId: IdSchema, payload: PtyStatePayloadSchema }, "proxy_to_client"),
|
|
436
675
|
// Provider 语义状态,来自 Claude/Codex hook 等结构化事件,不从 PTY 字节推断
|
|
437
676
|
control("agent_status", { sessionId: IdSchema, payload: AgentStatusPayloadSchema }, "proxy_to_client"),
|
|
438
677
|
// 终端标题变化,proxy -> client
|
|
439
|
-
control("terminal_title", { sessionId: IdSchema, title:
|
|
678
|
+
control("terminal_title", { sessionId: IdSchema, title: z8.string() }, "proxy_to_client"),
|
|
440
679
|
// 终端尺寸变化,proxy -> client
|
|
441
|
-
control("terminal_resize", { sessionId: IdSchema, cols:
|
|
442
|
-
control("terminal_resize_request", { sessionId: IdSchema, cols:
|
|
680
|
+
control("terminal_resize", { sessionId: IdSchema, cols: z8.number().int().positive(), rows: z8.number().int().positive() }, "proxy_to_client"),
|
|
681
|
+
control("terminal_resize_request", { sessionId: IdSchema, cols: z8.number().int().positive(), rows: z8.number().int().positive() }, "client_to_proxy"),
|
|
443
682
|
// 远程终止 JSON 会话,client -> proxy
|
|
444
683
|
control("session_terminate", { sessionId: IdSchema }, "client_to_proxy"),
|
|
445
|
-
control("session_rename", { ...RequestIdShape, sessionId: IdSchema, name:
|
|
684
|
+
control("session_rename", { ...RequestIdShape, sessionId: IdSchema, name: z8.string() }, "client_to_proxy"),
|
|
446
685
|
control("session_rename_response", {
|
|
447
686
|
...RequestIdShape,
|
|
448
687
|
sessionId: IdSchema,
|
|
449
|
-
success:
|
|
450
|
-
name:
|
|
688
|
+
success: z8.boolean(),
|
|
689
|
+
name: z8.string().optional(),
|
|
451
690
|
...RequestErrorShape
|
|
452
691
|
}, "proxy_to_client"),
|
|
453
692
|
// 中断当前 turn,client -> proxy,SIGINT 到 worker 进程让 claude CLI abort 当前流
|
|
@@ -455,114 +694,117 @@ var relayControlDefinitions = [
|
|
|
455
694
|
// turn 完成信号,proxy -> client,对应 claude stream-json 的 result 事件
|
|
456
695
|
control("turn_result", {
|
|
457
696
|
sessionId: IdSchema,
|
|
458
|
-
success:
|
|
459
|
-
isError:
|
|
697
|
+
success: z8.boolean(),
|
|
698
|
+
isError: z8.boolean(),
|
|
460
699
|
// stream-json result.result 是本轮最终文本。assistant_message 流丢失或 CLI 未发增量时,
|
|
461
700
|
// Web 用它作为 JSON 模式兜底展示,避免 turn 已结束但界面空白。
|
|
462
|
-
result:
|
|
701
|
+
result: z8.string().optional()
|
|
463
702
|
}, "proxy_to_client"),
|
|
464
703
|
// 客户端发送到 PTY 的原始字节(ANSI 序列),不追加换行
|
|
465
|
-
control("remote_input_raw", { sessionId: IdSchema, data:
|
|
704
|
+
control("remote_input_raw", { sessionId: IdSchema, data: z8.string(), traceId: IdSchema.optional() }, "client_to_proxy"),
|
|
466
705
|
control("clipboard_image_upload", {
|
|
467
706
|
...RequestIdShape,
|
|
468
707
|
sessionId: IdSchema,
|
|
469
708
|
mimeType: ClipboardImageMimeTypeSchema,
|
|
470
|
-
dataBase64:
|
|
471
|
-
fileName:
|
|
709
|
+
dataBase64: z8.string().min(1),
|
|
710
|
+
fileName: z8.string().optional()
|
|
472
711
|
}, "client_to_proxy"),
|
|
473
712
|
control("clipboard_image_upload_response", {
|
|
474
713
|
...RequestIdShape,
|
|
475
714
|
...RequestErrorShape,
|
|
476
715
|
sessionId: IdSchema,
|
|
477
|
-
success:
|
|
716
|
+
success: z8.boolean(),
|
|
478
717
|
// success=false 时 proxy 没有有效 path 可填;保持 optional 以避免占位空字符串通过校验。
|
|
479
|
-
path:
|
|
718
|
+
path: z8.string().optional()
|
|
480
719
|
}, "proxy_to_client"),
|
|
481
720
|
control("image_preview_request", {
|
|
482
721
|
...RequestIdShape,
|
|
483
722
|
sessionId: IdSchema,
|
|
484
|
-
path:
|
|
723
|
+
path: z8.string().min(1)
|
|
485
724
|
}, "client_to_proxy"),
|
|
486
725
|
control("image_preview_response", {
|
|
487
726
|
...RequestIdShape,
|
|
488
727
|
...RequestErrorShape,
|
|
489
728
|
sessionId: IdSchema,
|
|
490
|
-
success:
|
|
729
|
+
success: z8.boolean(),
|
|
491
730
|
// 同 clipboard_image_upload_response:失败时 proxy 不一定有路径。
|
|
492
|
-
path:
|
|
731
|
+
path: z8.string().optional(),
|
|
493
732
|
mimeType: ClipboardImageMimeTypeSchema.optional(),
|
|
494
|
-
dataBase64:
|
|
495
|
-
size:
|
|
733
|
+
dataBase64: z8.string().optional(),
|
|
734
|
+
size: z8.number().int().nonnegative().optional()
|
|
496
735
|
}, "proxy_to_client"),
|
|
497
736
|
// 任意文件下载: 与 image_preview 形状对称, 只是 mimeType 不限定为图片;
|
|
498
737
|
// 单租户场景下 path 任意 (不受 previewRoots 限制), 由 proxy 端 size cap 兜底。
|
|
499
738
|
control("file_download_request", {
|
|
500
739
|
...RequestIdShape,
|
|
501
740
|
sessionId: IdSchema,
|
|
502
|
-
path:
|
|
741
|
+
path: z8.string().min(1)
|
|
503
742
|
}, "client_to_proxy"),
|
|
504
743
|
control("file_download_response", {
|
|
505
744
|
...RequestIdShape,
|
|
506
745
|
...RequestErrorShape,
|
|
507
746
|
sessionId: IdSchema,
|
|
508
|
-
success:
|
|
509
|
-
path:
|
|
510
|
-
mimeType:
|
|
511
|
-
dataBase64:
|
|
512
|
-
size:
|
|
747
|
+
success: z8.boolean(),
|
|
748
|
+
path: z8.string().optional(),
|
|
749
|
+
mimeType: z8.string().optional(),
|
|
750
|
+
dataBase64: z8.string().optional(),
|
|
751
|
+
size: z8.number().int().nonnegative().optional()
|
|
513
752
|
}, "proxy_to_client"),
|
|
514
753
|
// 任意文件上传: 复用 clipboard_image_upload 的形状, mimeType 放开 + fileName 必填,
|
|
515
754
|
// 由 proxy 端写入 session cwd 的 .dev-anywhere/uploads/ 子目录, 返回相对路径供 web 拼成 @path。
|
|
516
755
|
control("file_upload_request", {
|
|
517
756
|
...RequestIdShape,
|
|
518
757
|
sessionId: IdSchema,
|
|
519
|
-
mimeType:
|
|
520
|
-
dataBase64:
|
|
521
|
-
fileName:
|
|
758
|
+
mimeType: z8.string().min(1),
|
|
759
|
+
dataBase64: z8.string().min(1),
|
|
760
|
+
fileName: z8.string().min(1)
|
|
522
761
|
}, "client_to_proxy"),
|
|
523
762
|
control("file_upload_response", {
|
|
524
763
|
...RequestIdShape,
|
|
525
764
|
...RequestErrorShape,
|
|
526
765
|
sessionId: IdSchema,
|
|
527
|
-
success:
|
|
528
|
-
path:
|
|
766
|
+
success: z8.boolean(),
|
|
767
|
+
path: z8.string().optional()
|
|
529
768
|
}, "proxy_to_client"),
|
|
530
769
|
// 客户端询问 proxy 的环境信息 (home 路径等), client -> proxy -> response
|
|
531
770
|
// FilePathPicker 用 homePath 作为 select 模式下的默认起点, 新建会话时打开即可浏览
|
|
532
771
|
control("proxy_info_request", RequestIdShape, "client_to_proxy"),
|
|
533
|
-
control("proxy_info", { ...RequestIdShape, homePath:
|
|
534
|
-
control("agent_cli_config_update", { ...RequestIdShape, provider:
|
|
772
|
+
control("proxy_info", { ...RequestIdShape, homePath: z8.string(), agentCli: AgentCliStatusSchema }, "proxy_to_client"),
|
|
773
|
+
control("agent_cli_config_update", { ...RequestIdShape, provider: z8.enum(providerValues), path: z8.string().min(1) }, "client_to_proxy"),
|
|
535
774
|
control("agent_cli_config_update_response", {
|
|
536
775
|
...RequestIdShape,
|
|
537
|
-
provider:
|
|
776
|
+
provider: z8.enum(providerValues),
|
|
538
777
|
agentCli: AgentCliStatusSchema.optional(),
|
|
539
778
|
...RequestErrorShape
|
|
540
779
|
}, "proxy_to_client"),
|
|
541
780
|
// 远程创建 JSON 会话,client -> proxy -> response
|
|
542
781
|
control("session_create", {
|
|
543
782
|
...RequestIdShape,
|
|
544
|
-
cwd:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
783
|
+
cwd: z8.string(),
|
|
784
|
+
name: z8.string().optional(),
|
|
785
|
+
provider: z8.enum(providerValues),
|
|
786
|
+
mode: z8.enum(sessionModeValues).optional(),
|
|
787
|
+
resumeSessionId: z8.string().optional(),
|
|
548
788
|
// 透传给 claude CLI 的 --permission-mode, undefined 时 proxy 兜底为 "default"
|
|
549
|
-
permissionMode:
|
|
789
|
+
permissionMode: z8.enum(["default", "auto", "acceptEdits", "plan", "bypassPermissions", "dontAsk"]).optional()
|
|
550
790
|
}, "client_to_proxy"),
|
|
551
791
|
control("session_create_response", {
|
|
552
792
|
...RequestIdShape,
|
|
553
793
|
// 失败路径只送 errorCode/error, sessionId 此时无语义。成功路径才有 id。
|
|
554
794
|
sessionId: IdSchema.optional(),
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
795
|
+
name: z8.string().optional(),
|
|
796
|
+
nameLocked: z8.boolean().optional(),
|
|
797
|
+
mode: z8.enum(sessionModeValues).optional(),
|
|
798
|
+
provider: z8.enum(providerValues).optional(),
|
|
799
|
+
ptyOwner: z8.enum(ptyOwnerValues).optional(),
|
|
558
800
|
...RequestErrorShape
|
|
559
801
|
}, "proxy_to_client"),
|
|
560
802
|
// 客户端请求会话历史消息,client -> proxy
|
|
561
803
|
control("session_messages_request", {
|
|
562
804
|
...RequestIdShape,
|
|
563
805
|
sessionId: IdSchema,
|
|
564
|
-
limit:
|
|
565
|
-
before:
|
|
806
|
+
limit: z8.number().int().min(1).max(200).optional(),
|
|
807
|
+
before: z8.string().optional()
|
|
566
808
|
}, "client_to_proxy"),
|
|
567
809
|
// 客户端请求会话资源(命令列表 + 文件树),client -> proxy
|
|
568
810
|
control("session_resources_request", { ...RequestIdShape, sessionId: IdSchema }, "client_to_proxy"),
|
|
@@ -570,14 +812,14 @@ var relayControlDefinitions = [
|
|
|
570
812
|
...RequestIdShape,
|
|
571
813
|
...RequestErrorShape,
|
|
572
814
|
sessionId: IdSchema,
|
|
573
|
-
commands:
|
|
574
|
-
groups:
|
|
815
|
+
commands: z8.array(CommandEntrySchema),
|
|
816
|
+
groups: z8.array(FileTreeGroupSchema)
|
|
575
817
|
}, "proxy_to_client"),
|
|
576
818
|
// 客户端请求当前 provider 语义状态;不经 relay 缓存,由 proxy 返回当前值
|
|
577
819
|
control("agent_status_request", { ...RequestIdShape, sessionId: IdSchema.optional() }, "client_to_proxy"),
|
|
578
820
|
control("agent_status_response", {
|
|
579
821
|
...RequestIdShape,
|
|
580
|
-
statuses:
|
|
822
|
+
statuses: z8.array(z8.object({ sessionId: IdSchema, payload: AgentStatusPayloadSchema }))
|
|
581
823
|
}, "proxy_to_client"),
|
|
582
824
|
// 客户端确认已收到审批请求;proxy 只记录送达状态,不把它当成用户决策
|
|
583
825
|
control("permission_request_delivered", { sessionId: IdSchema, requestId: IdSchema }, "client_to_proxy"),
|
|
@@ -587,40 +829,56 @@ var relayControlDefinitions = [
|
|
|
587
829
|
control("permission_decision_result", {
|
|
588
830
|
sessionId: IdSchema,
|
|
589
831
|
requestId: IdSchema,
|
|
590
|
-
outcome:
|
|
591
|
-
delivered:
|
|
592
|
-
message:
|
|
832
|
+
outcome: z8.enum(["allow", "deny"]),
|
|
833
|
+
delivered: z8.boolean(),
|
|
834
|
+
message: z8.string().optional()
|
|
593
835
|
}, "proxy_to_client"),
|
|
594
836
|
// proxy 推送当前 pending 的工具审批列表,client 据此恢复审批卡片
|
|
595
837
|
control("pending_approvals_push", {
|
|
596
838
|
sessionId: IdSchema,
|
|
597
|
-
approvals:
|
|
839
|
+
approvals: z8.array(z8.object({
|
|
598
840
|
requestId: IdSchema,
|
|
599
|
-
toolName:
|
|
600
|
-
input:
|
|
841
|
+
toolName: z8.string(),
|
|
842
|
+
input: z8.record(z8.string(), z8.unknown())
|
|
601
843
|
}))
|
|
602
844
|
}, "proxy_to_client"),
|
|
845
|
+
// Voice Pilot speech summaries are produced by proxy-side Claude Code so it can read project context.
|
|
846
|
+
control("voice_summary_request", {
|
|
847
|
+
...RequestIdShape,
|
|
848
|
+
sessionId: IdSchema,
|
|
849
|
+
messageId: IdSchema,
|
|
850
|
+
text: z8.string().min(1),
|
|
851
|
+
reason: VoiceSummaryReasonSchema
|
|
852
|
+
}, "client_to_proxy"),
|
|
853
|
+
control("voice_summary_response", {
|
|
854
|
+
...RequestIdShape,
|
|
855
|
+
...RequestErrorShape,
|
|
856
|
+
sessionId: IdSchema,
|
|
857
|
+
messageId: IdSchema,
|
|
858
|
+
success: z8.boolean(),
|
|
859
|
+
summary: z8.string().min(1).optional()
|
|
860
|
+
}, "proxy_to_client"),
|
|
603
861
|
// 恢复会话时推送历史消息,proxy -> client
|
|
604
862
|
control("session_history_messages", {
|
|
605
863
|
...RequestIdShape,
|
|
606
864
|
sessionId: IdSchema,
|
|
607
|
-
before:
|
|
608
|
-
messages:
|
|
609
|
-
hasMore:
|
|
610
|
-
nextBefore:
|
|
865
|
+
before: z8.string().optional(),
|
|
866
|
+
messages: z8.array(SessionHistoryMessageSchema),
|
|
867
|
+
hasMore: z8.boolean().optional(),
|
|
868
|
+
nextBefore: z8.string().optional()
|
|
611
869
|
}, "proxy_to_client"),
|
|
612
870
|
// proxy 重连后同步活跃 session 列表给 relay。session_sync 由 relay 自消费(更新 proxy-session
|
|
613
871
|
// 关联)不转发给 client,因此**没有** direction 标注——RelayControlDirection 只描述转发流。
|
|
614
872
|
control("session_sync", {
|
|
615
|
-
sessions:
|
|
616
|
-
id:
|
|
617
|
-
mode:
|
|
618
|
-
provider:
|
|
619
|
-
ptyOwner:
|
|
620
|
-
cwd:
|
|
621
|
-
name:
|
|
622
|
-
nameLocked:
|
|
623
|
-
state:
|
|
873
|
+
sessions: z8.array(z8.object({
|
|
874
|
+
id: z8.string(),
|
|
875
|
+
mode: z8.enum(sessionModeValues),
|
|
876
|
+
provider: z8.enum(providerValues),
|
|
877
|
+
ptyOwner: z8.enum(ptyOwnerValues).optional(),
|
|
878
|
+
cwd: z8.string().optional(),
|
|
879
|
+
name: z8.string().optional(),
|
|
880
|
+
nameLocked: z8.boolean().optional(),
|
|
881
|
+
state: z8.enum(sessionStateValues)
|
|
624
882
|
}))
|
|
625
883
|
}),
|
|
626
884
|
// PTY 会话订阅,client -> proxy,触发 terminal serialize() 返回当前状态
|
|
@@ -628,15 +886,15 @@ var relayControlDefinitions = [
|
|
|
628
886
|
// PTY 会话快照,proxy -> client,serialize() 的全量终端状态
|
|
629
887
|
control("session_snapshot", {
|
|
630
888
|
sessionId: IdSchema,
|
|
631
|
-
cols:
|
|
632
|
-
rows:
|
|
633
|
-
data:
|
|
634
|
-
outputSeq:
|
|
889
|
+
cols: z8.number().int().positive(),
|
|
890
|
+
rows: z8.number().int().positive(),
|
|
891
|
+
data: z8.string(),
|
|
892
|
+
outputSeq: z8.number().int().nonnegative(),
|
|
635
893
|
requestId: IdSchema.optional()
|
|
636
894
|
}, "proxy_to_client")
|
|
637
895
|
];
|
|
638
896
|
var relayControlSchemas = relayControlDefinitions.map((definition) => definition.schema);
|
|
639
|
-
var RelayControlSchema =
|
|
897
|
+
var RelayControlSchema = z8.discriminatedUnion("type", relayControlSchemas);
|
|
640
898
|
var ProxyToClientRelayControlTypes = new Set(relayControlDefinitions.filter((definition) => definition.directions.has("proxy_to_client")).map((definition) => definition.type));
|
|
641
899
|
function isProxyToClientRelayControlType(type) {
|
|
642
900
|
return ProxyToClientRelayControlTypes.has(type);
|
|
@@ -1275,8 +1533,656 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1275
1533
|
}
|
|
1276
1534
|
|
|
1277
1535
|
// src/handlers/client.ts
|
|
1278
|
-
import { WebSocket as
|
|
1536
|
+
import { WebSocket as WebSocket6 } from "ws";
|
|
1537
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
1538
|
+
|
|
1539
|
+
// src/voice/bailian-asr.ts
|
|
1540
|
+
import { EventEmitter } from "events";
|
|
1279
1541
|
import { nanoid } from "nanoid";
|
|
1542
|
+
import { WebSocket as WebSocket4 } from "ws";
|
|
1543
|
+
|
|
1544
|
+
// src/voice/bailian-endpoints.ts
|
|
1545
|
+
var BAILIAN_HOSTS = {
|
|
1546
|
+
cn: "wss://dashscope.aliyuncs.com",
|
|
1547
|
+
intl: "wss://dashscope-intl.aliyuncs.com"
|
|
1548
|
+
};
|
|
1549
|
+
function bailianRealtimeUrl(region, model) {
|
|
1550
|
+
const url = `${BAILIAN_HOSTS[region]}/api-ws/v1/realtime`;
|
|
1551
|
+
return model ? `${url}?model=${encodeURIComponent(model)}` : url;
|
|
1552
|
+
}
|
|
1553
|
+
function bailianInferenceUrl(region) {
|
|
1554
|
+
return `${BAILIAN_HOSTS[region]}/api-ws/v1/inference`;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/voice/bailian-asr.ts
|
|
1558
|
+
var OPEN = 1;
|
|
1559
|
+
var END_OF_SPEECH_SILENCE_MS = 1200;
|
|
1560
|
+
function defaultSocketFactory(url, options) {
|
|
1561
|
+
return new WebSocket4(url, options);
|
|
1562
|
+
}
|
|
1563
|
+
function extractRealtimePreview(payload) {
|
|
1564
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1565
|
+
const record = payload;
|
|
1566
|
+
if (typeof record.text === "string" || typeof record.stash === "string") {
|
|
1567
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
1568
|
+
const stash = typeof record.stash === "string" ? record.stash : "";
|
|
1569
|
+
const preview = `${text}${stash}`;
|
|
1570
|
+
return preview.length > 0 ? preview : null;
|
|
1571
|
+
}
|
|
1572
|
+
const candidates = [
|
|
1573
|
+
record.text,
|
|
1574
|
+
record.transcript,
|
|
1575
|
+
record.delta,
|
|
1576
|
+
record.output && typeof record.output === "object" && record.output.text
|
|
1577
|
+
];
|
|
1578
|
+
for (const candidate of candidates) {
|
|
1579
|
+
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
1580
|
+
}
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
function extractFinalText(payload) {
|
|
1584
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1585
|
+
const record = payload;
|
|
1586
|
+
const candidates = [
|
|
1587
|
+
record.transcript,
|
|
1588
|
+
record.text,
|
|
1589
|
+
record.output && typeof record.output === "object" && record.output.text
|
|
1590
|
+
];
|
|
1591
|
+
for (const candidate of candidates) {
|
|
1592
|
+
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
1593
|
+
}
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
function extractError(payload) {
|
|
1597
|
+
if (!payload || typeof payload !== "object") return new Error("Bailian ASR error");
|
|
1598
|
+
const record = payload;
|
|
1599
|
+
const nested = record.error && typeof record.error === "object" ? record.error : null;
|
|
1600
|
+
let message = "Bailian ASR error";
|
|
1601
|
+
if (nested && typeof nested.message === "string") {
|
|
1602
|
+
message = nested.message;
|
|
1603
|
+
} else if (typeof record.message === "string") {
|
|
1604
|
+
message = record.message;
|
|
1605
|
+
}
|
|
1606
|
+
return new Error(message);
|
|
1607
|
+
}
|
|
1608
|
+
var BailianAsrClientImpl = class extends EventEmitter {
|
|
1609
|
+
constructor(config, socketFactory, eventIdFactory) {
|
|
1610
|
+
super();
|
|
1611
|
+
this.config = config;
|
|
1612
|
+
this.eventIdFactory = eventIdFactory;
|
|
1613
|
+
this.socket = socketFactory(bailianRealtimeUrl(config.region, config.model), {
|
|
1614
|
+
headers: {
|
|
1615
|
+
Authorization: `bearer ${config.apiKey}`,
|
|
1616
|
+
"OpenAI-Beta": "realtime=v1"
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
this.socket.on("open", () => this.handleOpen());
|
|
1620
|
+
this.socket.on("message", (data) => this.handleMessage(data));
|
|
1621
|
+
this.socket.on(
|
|
1622
|
+
"error",
|
|
1623
|
+
(err) => this.emit("error", err instanceof Error ? err : new Error(String(err)))
|
|
1624
|
+
);
|
|
1625
|
+
this.socket.on(
|
|
1626
|
+
"close",
|
|
1627
|
+
(code, reason) => this.emit("closed", code, reason?.toString("utf8"))
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
config;
|
|
1631
|
+
eventIdFactory;
|
|
1632
|
+
socket;
|
|
1633
|
+
isOpen = false;
|
|
1634
|
+
isReady = false;
|
|
1635
|
+
pending = [];
|
|
1636
|
+
sendPcm(chunk) {
|
|
1637
|
+
this.sendWhenReady({
|
|
1638
|
+
event_id: this.eventIdFactory(),
|
|
1639
|
+
type: "input_audio_buffer.append",
|
|
1640
|
+
audio: chunk.toString("base64")
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
stop() {
|
|
1644
|
+
this.sendWhenReady({
|
|
1645
|
+
event_id: this.eventIdFactory(),
|
|
1646
|
+
type: "session.finish"
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
close() {
|
|
1650
|
+
this.socket.close();
|
|
1651
|
+
}
|
|
1652
|
+
handleOpen() {
|
|
1653
|
+
this.isOpen = true;
|
|
1654
|
+
this.sendNow({
|
|
1655
|
+
event_id: this.eventIdFactory(),
|
|
1656
|
+
type: "session.update",
|
|
1657
|
+
session: {
|
|
1658
|
+
modalities: ["text"],
|
|
1659
|
+
input_audio_format: "pcm",
|
|
1660
|
+
sample_rate: this.config.sampleRate,
|
|
1661
|
+
input_audio_transcription: {
|
|
1662
|
+
language: this.config.language
|
|
1663
|
+
},
|
|
1664
|
+
turn_detection: {
|
|
1665
|
+
type: "server_vad",
|
|
1666
|
+
threshold: 0,
|
|
1667
|
+
silence_duration_ms: END_OF_SPEECH_SILENCE_MS
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
handleMessage(data) {
|
|
1673
|
+
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString("utf8") : "";
|
|
1674
|
+
if (!text) return;
|
|
1675
|
+
let payload;
|
|
1676
|
+
try {
|
|
1677
|
+
payload = JSON.parse(text);
|
|
1678
|
+
} catch {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const record = payload;
|
|
1682
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
1683
|
+
if (type.includes("error")) {
|
|
1684
|
+
this.emit("error", extractError(payload));
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (type === "conversation.item.input_audio_transcription.failed") {
|
|
1688
|
+
this.emit("error", extractError(payload));
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (type === "session.updated") {
|
|
1692
|
+
this.isReady = true;
|
|
1693
|
+
this.emit("ready");
|
|
1694
|
+
this.flushPending();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (type === "conversation.item.input_audio_transcription.text") {
|
|
1698
|
+
const preview = extractRealtimePreview(payload);
|
|
1699
|
+
if (preview) this.emit("partial", preview);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (type === "conversation.item.input_audio_transcription.completed" || type === "session.finished") {
|
|
1703
|
+
const transcript = extractFinalText(payload);
|
|
1704
|
+
if (transcript) {
|
|
1705
|
+
this.emit("final", transcript);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
sendWhenReady(payload) {
|
|
1710
|
+
const message = JSON.stringify(payload);
|
|
1711
|
+
if ((this.isOpen || this.socket.readyState === OPEN) && this.isReady) {
|
|
1712
|
+
this.socket.send(message);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
this.pending.push(message);
|
|
1716
|
+
}
|
|
1717
|
+
sendNow(payload) {
|
|
1718
|
+
this.socket.send(JSON.stringify(payload));
|
|
1719
|
+
}
|
|
1720
|
+
flushPending() {
|
|
1721
|
+
for (const message of this.pending) {
|
|
1722
|
+
this.socket.send(message);
|
|
1723
|
+
}
|
|
1724
|
+
this.pending = [];
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
function createBailianAsrClient(config, options = {}) {
|
|
1728
|
+
return new BailianAsrClientImpl(
|
|
1729
|
+
config,
|
|
1730
|
+
options.socketFactory ?? defaultSocketFactory,
|
|
1731
|
+
options.eventIdFactory ?? (() => `event_${nanoid()}`)
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/voice/bailian-tts.ts
|
|
1736
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1737
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
1738
|
+
import { WebSocket as WebSocket5 } from "ws";
|
|
1739
|
+
var OPEN2 = 1;
|
|
1740
|
+
function defaultSocketFactory2(url, options) {
|
|
1741
|
+
return new WebSocket5(url, options);
|
|
1742
|
+
}
|
|
1743
|
+
function errorFromPayload(payload) {
|
|
1744
|
+
if (!payload || typeof payload !== "object") return new Error("Bailian TTS error");
|
|
1745
|
+
const record = payload;
|
|
1746
|
+
const header = record.header && typeof record.header === "object" ? record.header : null;
|
|
1747
|
+
let message = "Bailian TTS error";
|
|
1748
|
+
if (header && typeof header.error_message === "string") {
|
|
1749
|
+
message = header.error_message;
|
|
1750
|
+
} else if (typeof record.message === "string") {
|
|
1751
|
+
message = record.message;
|
|
1752
|
+
}
|
|
1753
|
+
return new Error(message);
|
|
1754
|
+
}
|
|
1755
|
+
function eventFromPayload(payload) {
|
|
1756
|
+
if (!payload || typeof payload !== "object") return "";
|
|
1757
|
+
const record = payload;
|
|
1758
|
+
const header = record.header && typeof record.header === "object" ? record.header : null;
|
|
1759
|
+
const event = header ? header.event : void 0;
|
|
1760
|
+
return typeof event === "string" ? event : "";
|
|
1761
|
+
}
|
|
1762
|
+
var BailianTtsClientImpl = class extends EventEmitter2 {
|
|
1763
|
+
constructor(config, socketFactory, taskIdFactory) {
|
|
1764
|
+
super();
|
|
1765
|
+
this.config = config;
|
|
1766
|
+
this.taskIdFactory = taskIdFactory;
|
|
1767
|
+
this.socket = socketFactory(bailianInferenceUrl(config.region), {
|
|
1768
|
+
headers: { Authorization: `bearer ${config.apiKey}` }
|
|
1769
|
+
});
|
|
1770
|
+
this.socket.on("open", () => this.handleOpen());
|
|
1771
|
+
this.socket.on(
|
|
1772
|
+
"message",
|
|
1773
|
+
(data, isBinary) => this.handleMessage(data, isBinary)
|
|
1774
|
+
);
|
|
1775
|
+
this.socket.on(
|
|
1776
|
+
"error",
|
|
1777
|
+
(err) => this.emit("error", err instanceof Error ? err : new Error(String(err)))
|
|
1778
|
+
);
|
|
1779
|
+
this.socket.on(
|
|
1780
|
+
"close",
|
|
1781
|
+
(code, reason) => this.emit("closed", code, reason?.toString("utf8"))
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
config;
|
|
1785
|
+
taskIdFactory;
|
|
1786
|
+
socket;
|
|
1787
|
+
isOpen = false;
|
|
1788
|
+
current = null;
|
|
1789
|
+
speak(text) {
|
|
1790
|
+
if (this.current) {
|
|
1791
|
+
throw new Error("Bailian TTS is already speaking");
|
|
1792
|
+
}
|
|
1793
|
+
this.current = { taskId: this.taskIdFactory(), text };
|
|
1794
|
+
if (this.isOpen || this.socket.readyState === OPEN2) {
|
|
1795
|
+
this.sendRunTask();
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
close() {
|
|
1799
|
+
this.socket.close();
|
|
1800
|
+
}
|
|
1801
|
+
handleOpen() {
|
|
1802
|
+
this.isOpen = true;
|
|
1803
|
+
if (this.current) this.sendRunTask();
|
|
1804
|
+
}
|
|
1805
|
+
handleMessage(data, isBinary = false) {
|
|
1806
|
+
const text = this.tryDecodeText(data, isBinary);
|
|
1807
|
+
if (!text) {
|
|
1808
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
1809
|
+
this.emit("audio", chunk);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
let payload;
|
|
1813
|
+
try {
|
|
1814
|
+
payload = JSON.parse(text);
|
|
1815
|
+
} catch {
|
|
1816
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(text);
|
|
1817
|
+
this.emit("audio", chunk);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
const event = eventFromPayload(payload);
|
|
1821
|
+
if (event === "task-started") {
|
|
1822
|
+
this.emit("started");
|
|
1823
|
+
this.sendTextAndFinish();
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
if (event === "task-finished") {
|
|
1827
|
+
this.emit("finished");
|
|
1828
|
+
this.current = null;
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (event === "task-failed" || event === "task-error") {
|
|
1832
|
+
this.emit("error", errorFromPayload(payload));
|
|
1833
|
+
this.current = null;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
tryDecodeText(data, isBinary) {
|
|
1837
|
+
if (typeof data === "string") return data;
|
|
1838
|
+
if (isBinary) return null;
|
|
1839
|
+
if (Buffer.isBuffer(data)) return data.toString("utf8");
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
sendRunTask() {
|
|
1843
|
+
if (!this.current) return;
|
|
1844
|
+
this.socket.send(
|
|
1845
|
+
JSON.stringify({
|
|
1846
|
+
header: {
|
|
1847
|
+
action: "run-task",
|
|
1848
|
+
task_id: this.current.taskId,
|
|
1849
|
+
streaming: "duplex"
|
|
1850
|
+
},
|
|
1851
|
+
payload: {
|
|
1852
|
+
task_group: "audio",
|
|
1853
|
+
task: "tts",
|
|
1854
|
+
function: "SpeechSynthesizer",
|
|
1855
|
+
model: this.config.model,
|
|
1856
|
+
input: {},
|
|
1857
|
+
parameters: {
|
|
1858
|
+
text_type: "PlainText",
|
|
1859
|
+
voice: this.config.voice,
|
|
1860
|
+
format: "pcm",
|
|
1861
|
+
sample_rate: this.config.sampleRate
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
})
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
sendTextAndFinish() {
|
|
1868
|
+
if (!this.current) return;
|
|
1869
|
+
this.socket.send(
|
|
1870
|
+
JSON.stringify({
|
|
1871
|
+
header: { action: "continue-task", task_id: this.current.taskId },
|
|
1872
|
+
payload: { input: { text: this.current.text } }
|
|
1873
|
+
})
|
|
1874
|
+
);
|
|
1875
|
+
this.socket.send(
|
|
1876
|
+
JSON.stringify({
|
|
1877
|
+
header: { action: "finish-task", task_id: this.current.taskId },
|
|
1878
|
+
payload: { input: {} }
|
|
1879
|
+
})
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
function createBailianTtsClient(config, options = {}) {
|
|
1884
|
+
return new BailianTtsClientImpl(
|
|
1885
|
+
config,
|
|
1886
|
+
options.socketFactory ?? defaultSocketFactory2,
|
|
1887
|
+
options.taskIdFactory ?? (() => nanoid2())
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// src/voice/config-test.ts
|
|
1892
|
+
var TEST_SAMPLE_RATE = 16e3;
|
|
1893
|
+
var ASR_TEST_CHUNK_BYTES = 3200;
|
|
1894
|
+
var ASR_TEST_CHUNK_INTERVAL_MS = 100;
|
|
1895
|
+
function mergeVoiceConfigForTest(current, update) {
|
|
1896
|
+
return {
|
|
1897
|
+
...current,
|
|
1898
|
+
provider: "aliyun-bailian",
|
|
1899
|
+
...update?.clearApiKey ? { apiKey: void 0 } : {},
|
|
1900
|
+
...update?.apiKey ? { apiKey: update.apiKey } : {},
|
|
1901
|
+
...update?.region ? { region: update.region } : {},
|
|
1902
|
+
...update?.asrModel ? { asrModel: update.asrModel } : {},
|
|
1903
|
+
...update?.ttsModel ? { ttsModel: update.ttsModel } : {},
|
|
1904
|
+
...update?.ttsVoice ? { ttsVoice: update.ttsVoice } : {},
|
|
1905
|
+
...update?.turnIdleSeconds ? { turnIdleSeconds: update.turnIdleSeconds } : {}
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
function createBailianVoiceConfigTester(options = {}) {
|
|
1909
|
+
const ttsClientFactory = options.ttsClientFactory ?? createBailianTtsClient;
|
|
1910
|
+
const asrClientFactory = options.asrClientFactory ?? createBailianAsrClient;
|
|
1911
|
+
const sampleText = options.sampleText ?? "\u8BED\u97F3\u52A9\u624B\u6D4B\u8BD5";
|
|
1912
|
+
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
1913
|
+
return {
|
|
1914
|
+
async test(config) {
|
|
1915
|
+
if (!config.apiKey) {
|
|
1916
|
+
return Promise.reject(new Error("\u8BF7\u5148\u586B\u5199\u963F\u91CC\u4E91\u767E\u70BC API Key"));
|
|
1917
|
+
}
|
|
1918
|
+
const audio = await synthesizeTestAudio({
|
|
1919
|
+
config,
|
|
1920
|
+
sampleText,
|
|
1921
|
+
timeoutMs,
|
|
1922
|
+
clientFactory: ttsClientFactory
|
|
1923
|
+
});
|
|
1924
|
+
const transcript = await recognizeTestAudio({
|
|
1925
|
+
config,
|
|
1926
|
+
audio,
|
|
1927
|
+
sampleText,
|
|
1928
|
+
timeoutMs,
|
|
1929
|
+
clientFactory: asrClientFactory
|
|
1930
|
+
});
|
|
1931
|
+
return { audio, sampleRate: TEST_SAMPLE_RATE, transcript };
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function synthesizeTestAudio(options) {
|
|
1936
|
+
const { config, sampleText, timeoutMs, clientFactory } = options;
|
|
1937
|
+
const client = clientFactory({
|
|
1938
|
+
apiKey: config.apiKey,
|
|
1939
|
+
region: config.region,
|
|
1940
|
+
model: config.ttsModel,
|
|
1941
|
+
voice: config.ttsVoice,
|
|
1942
|
+
sampleRate: TEST_SAMPLE_RATE
|
|
1943
|
+
});
|
|
1944
|
+
return new Promise((resolve2, reject) => {
|
|
1945
|
+
let settled = false;
|
|
1946
|
+
const chunks = [];
|
|
1947
|
+
const timer = setTimeout(() => {
|
|
1948
|
+
settle(new Error("TTS \u6D4B\u8BD5\u8D85\u65F6"));
|
|
1949
|
+
}, timeoutMs);
|
|
1950
|
+
function settle(error, audio) {
|
|
1951
|
+
if (settled) return;
|
|
1952
|
+
settled = true;
|
|
1953
|
+
clearTimeout(timer);
|
|
1954
|
+
client.close();
|
|
1955
|
+
if (error) reject(error);
|
|
1956
|
+
else resolve2(audio ?? Buffer.alloc(0));
|
|
1957
|
+
}
|
|
1958
|
+
client.on("audio", (chunk) => {
|
|
1959
|
+
if (chunk.length > 0) chunks.push(chunk);
|
|
1960
|
+
});
|
|
1961
|
+
client.on("finished", () => {
|
|
1962
|
+
if (chunks.length === 0) {
|
|
1963
|
+
settle(new Error("TTS \u6D4B\u8BD5\u6CA1\u6709\u8FD4\u56DE\u97F3\u9891"));
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
settle(void 0, Buffer.concat(chunks));
|
|
1967
|
+
});
|
|
1968
|
+
client.on("error", (error) => settle(error));
|
|
1969
|
+
client.on("closed", () => {
|
|
1970
|
+
if (!settled) settle(new Error("TTS \u6D4B\u8BD5\u8FDE\u63A5\u5DF2\u5173\u95ED"));
|
|
1971
|
+
});
|
|
1972
|
+
try {
|
|
1973
|
+
client.speak(sampleText);
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
settle(err instanceof Error ? err : new Error("TTS \u6D4B\u8BD5\u542F\u52A8\u5931\u8D25"));
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
function recognizeTestAudio(options) {
|
|
1980
|
+
const { config, audio, sampleText, timeoutMs, clientFactory } = options;
|
|
1981
|
+
const client = clientFactory({
|
|
1982
|
+
apiKey: config.apiKey,
|
|
1983
|
+
region: config.region,
|
|
1984
|
+
model: config.asrModel,
|
|
1985
|
+
sampleRate: TEST_SAMPLE_RATE,
|
|
1986
|
+
language: "zh"
|
|
1987
|
+
});
|
|
1988
|
+
return new Promise((resolve2, reject) => {
|
|
1989
|
+
let settled = false;
|
|
1990
|
+
let streamTimer = null;
|
|
1991
|
+
const timer = setTimeout(() => {
|
|
1992
|
+
settle(new Error("STT \u6D4B\u8BD5\u8D85\u65F6"));
|
|
1993
|
+
}, timeoutMs);
|
|
1994
|
+
function settle(error, transcript) {
|
|
1995
|
+
if (settled) return;
|
|
1996
|
+
settled = true;
|
|
1997
|
+
clearTimeout(timer);
|
|
1998
|
+
if (streamTimer) clearTimeout(streamTimer);
|
|
1999
|
+
client.close();
|
|
2000
|
+
if (error) reject(error);
|
|
2001
|
+
else resolve2(transcript ?? "");
|
|
2002
|
+
}
|
|
2003
|
+
client.on("ready", () => {
|
|
2004
|
+
let offset = 0;
|
|
2005
|
+
const sendNextChunk = () => {
|
|
2006
|
+
if (settled) return;
|
|
2007
|
+
const chunk = audio.subarray(offset, offset + ASR_TEST_CHUNK_BYTES);
|
|
2008
|
+
if (chunk.length > 0) {
|
|
2009
|
+
client.sendPcm(chunk);
|
|
2010
|
+
offset += chunk.length;
|
|
2011
|
+
}
|
|
2012
|
+
if (offset >= audio.length) {
|
|
2013
|
+
client.stop();
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
streamTimer = setTimeout(sendNextChunk, ASR_TEST_CHUNK_INTERVAL_MS);
|
|
2017
|
+
};
|
|
2018
|
+
sendNextChunk();
|
|
2019
|
+
});
|
|
2020
|
+
client.on("final", (transcript) => {
|
|
2021
|
+
if (matchesExpectedTranscript(transcript, sampleText)) {
|
|
2022
|
+
settle(void 0, transcript);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
settle(new Error(`STT \u6D4B\u8BD5\u8BC6\u522B\u7ED3\u679C\u4E0D\u5339\u914D\uFF1A${transcript}`));
|
|
2026
|
+
});
|
|
2027
|
+
client.on("error", (error) => settle(error));
|
|
2028
|
+
client.on("closed", () => {
|
|
2029
|
+
if (!settled) settle(new Error("STT \u6D4B\u8BD5\u8FDE\u63A5\u5DF2\u5173\u95ED"));
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
function matchesExpectedTranscript(actual, expected) {
|
|
2034
|
+
return normalizeTranscript(actual).includes(normalizeTranscript(expected));
|
|
2035
|
+
}
|
|
2036
|
+
function normalizeTranscript(text) {
|
|
2037
|
+
return text.replace(/[^\p{Script=Han}a-zA-Z0-9]/gu, "").toLowerCase();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/voice/client-controls.ts
|
|
2041
|
+
function handleVoiceConfigControl(msg, ws, store, logger, providers) {
|
|
2042
|
+
if (msg.type === "voice_config_request") {
|
|
2043
|
+
ws.send(
|
|
2044
|
+
JSON.stringify({
|
|
2045
|
+
type: "voice_config_response",
|
|
2046
|
+
requestId: msg.requestId,
|
|
2047
|
+
config: store.read()
|
|
2048
|
+
})
|
|
2049
|
+
);
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
if (msg.type === "voice_config_update") {
|
|
2053
|
+
try {
|
|
2054
|
+
const config = store.update(msg.config);
|
|
2055
|
+
ws.send(
|
|
2056
|
+
JSON.stringify({
|
|
2057
|
+
type: "voice_config_update_response",
|
|
2058
|
+
requestId: msg.requestId,
|
|
2059
|
+
success: true,
|
|
2060
|
+
config
|
|
2061
|
+
})
|
|
2062
|
+
);
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
logger.warn({ err }, "Voice config update failed");
|
|
2065
|
+
ws.send(
|
|
2066
|
+
JSON.stringify({
|
|
2067
|
+
type: "voice_config_update_response",
|
|
2068
|
+
requestId: msg.requestId,
|
|
2069
|
+
success: false,
|
|
2070
|
+
errorCode: ControlErrorCode.UNKNOWN,
|
|
2071
|
+
error: err instanceof Error ? err.message : "Voice config update failed"
|
|
2072
|
+
})
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
if (msg.type === "voice_capabilities_request") {
|
|
2078
|
+
if (!providers) {
|
|
2079
|
+
ws.send(
|
|
2080
|
+
JSON.stringify({
|
|
2081
|
+
type: "voice_capabilities_response",
|
|
2082
|
+
requestId: msg.requestId,
|
|
2083
|
+
errorCode: ControlErrorCode.UNKNOWN,
|
|
2084
|
+
error: "Voice capabilities provider is not available"
|
|
2085
|
+
})
|
|
2086
|
+
);
|
|
2087
|
+
return true;
|
|
2088
|
+
}
|
|
2089
|
+
const config = { ...store.readSecret(), ...msg.region ? { region: msg.region } : {} };
|
|
2090
|
+
let provider;
|
|
2091
|
+
try {
|
|
2092
|
+
provider = providers.current(config);
|
|
2093
|
+
} catch (err) {
|
|
2094
|
+
logger.warn({ err }, "Voice capabilities provider resolution failed");
|
|
2095
|
+
ws.send(
|
|
2096
|
+
JSON.stringify({
|
|
2097
|
+
type: "voice_capabilities_response",
|
|
2098
|
+
requestId: msg.requestId,
|
|
2099
|
+
errorCode: ControlErrorCode.UNKNOWN,
|
|
2100
|
+
error: err instanceof Error ? err.message : "Voice capabilities request failed"
|
|
2101
|
+
})
|
|
2102
|
+
);
|
|
2103
|
+
return true;
|
|
2104
|
+
}
|
|
2105
|
+
void provider.readCapabilities(config).then((capabilities) => {
|
|
2106
|
+
ws.send(
|
|
2107
|
+
JSON.stringify({
|
|
2108
|
+
type: "voice_capabilities_response",
|
|
2109
|
+
requestId: msg.requestId,
|
|
2110
|
+
capabilities
|
|
2111
|
+
})
|
|
2112
|
+
);
|
|
2113
|
+
}).catch((err) => {
|
|
2114
|
+
logger.warn({ err }, "Voice capabilities request failed");
|
|
2115
|
+
ws.send(
|
|
2116
|
+
JSON.stringify({
|
|
2117
|
+
type: "voice_capabilities_response",
|
|
2118
|
+
requestId: msg.requestId,
|
|
2119
|
+
errorCode: ControlErrorCode.UNKNOWN,
|
|
2120
|
+
error: err instanceof Error ? err.message : "Voice capabilities request failed"
|
|
2121
|
+
})
|
|
2122
|
+
);
|
|
2123
|
+
});
|
|
2124
|
+
return true;
|
|
2125
|
+
}
|
|
2126
|
+
if (msg.type === "voice_config_test") {
|
|
2127
|
+
if (!providers) {
|
|
2128
|
+
ws.send(
|
|
2129
|
+
JSON.stringify({
|
|
2130
|
+
type: "voice_config_test_response",
|
|
2131
|
+
requestId: msg.requestId,
|
|
2132
|
+
success: false,
|
|
2133
|
+
errorCode: ControlErrorCode.UNKNOWN,
|
|
2134
|
+
error: "Voice config tester is not available"
|
|
2135
|
+
})
|
|
2136
|
+
);
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
const testConfig = mergeVoiceConfigForTest(store.readSecret(), msg.config);
|
|
2140
|
+
let provider;
|
|
2141
|
+
try {
|
|
2142
|
+
provider = providers.current(testConfig);
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
logger.warn({ err }, "Voice config test provider resolution failed");
|
|
2145
|
+
ws.send(
|
|
2146
|
+
JSON.stringify({
|
|
2147
|
+
type: "voice_config_test_response",
|
|
2148
|
+
requestId: msg.requestId,
|
|
2149
|
+
success: false,
|
|
2150
|
+
errorCode: ControlErrorCode.UNKNOWN,
|
|
2151
|
+
error: err instanceof Error ? err.message : "Voice config test failed"
|
|
2152
|
+
})
|
|
2153
|
+
);
|
|
2154
|
+
return true;
|
|
2155
|
+
}
|
|
2156
|
+
void provider.testConfig(testConfig).then((result) => {
|
|
2157
|
+
ws.send(
|
|
2158
|
+
JSON.stringify({
|
|
2159
|
+
type: "voice_config_test_response",
|
|
2160
|
+
requestId: msg.requestId,
|
|
2161
|
+
success: true,
|
|
2162
|
+
...result.audio ? { audioBase64: result.audio.toString("base64") } : {},
|
|
2163
|
+
...result.sampleRate ? { audioSampleRate: result.sampleRate } : {},
|
|
2164
|
+
...result.audio ? { audioEncoding: "pcm_s16le" } : {},
|
|
2165
|
+
...result.transcript ? { transcript: result.transcript } : {}
|
|
2166
|
+
})
|
|
2167
|
+
);
|
|
2168
|
+
}).catch((err) => {
|
|
2169
|
+
logger.warn({ err }, "Voice config test failed");
|
|
2170
|
+
ws.send(
|
|
2171
|
+
JSON.stringify({
|
|
2172
|
+
type: "voice_config_test_response",
|
|
2173
|
+
requestId: msg.requestId,
|
|
2174
|
+
success: false,
|
|
2175
|
+
errorCode: ControlErrorCode.UNKNOWN,
|
|
2176
|
+
error: err instanceof Error ? err.message : "Voice config test failed"
|
|
2177
|
+
})
|
|
2178
|
+
);
|
|
2179
|
+
});
|
|
2180
|
+
return true;
|
|
2181
|
+
}
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// src/handlers/client.ts
|
|
1280
2186
|
var MAX_JSON_MESSAGE_SIZE2 = 1 * 1024 * 1024;
|
|
1281
2187
|
function handleClientRegister(clientId, clientWs, registry, logger) {
|
|
1282
2188
|
clientWs.clientId = clientId;
|
|
@@ -1334,7 +2240,7 @@ function rejectProxySelect(ws, requestId, proxyId) {
|
|
|
1334
2240
|
})
|
|
1335
2241
|
);
|
|
1336
2242
|
}
|
|
1337
|
-
function handleClientConnection(ws, registry, logger, chaos) {
|
|
2243
|
+
function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, voiceProviders) {
|
|
1338
2244
|
const clientWs = ws;
|
|
1339
2245
|
clientWs.isAlive = true;
|
|
1340
2246
|
registry.addClientWs(clientWs);
|
|
@@ -1384,6 +2290,9 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1384
2290
|
}
|
|
1385
2291
|
return;
|
|
1386
2292
|
}
|
|
2293
|
+
if (voiceConfigStore && handleVoiceConfigControl(msg, clientWs, voiceConfigStore, logger, voiceProviders)) {
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
1387
2296
|
if (isClientToProxyRelayControlType(msg.type)) {
|
|
1388
2297
|
const targetProxyId = clientWs.boundProxyId;
|
|
1389
2298
|
if (!targetProxyId) {
|
|
@@ -1391,7 +2300,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1391
2300
|
return;
|
|
1392
2301
|
}
|
|
1393
2302
|
const proxyWs = registry.getProxy(targetProxyId);
|
|
1394
|
-
if (proxyWs && proxyWs.readyState ===
|
|
2303
|
+
if (proxyWs && proxyWs.readyState === WebSocket6.OPEN) {
|
|
1395
2304
|
if (chaos) chaos.send(proxyWs, raw, { direction: "client_to_proxy", type: msg.type });
|
|
1396
2305
|
else proxyWs.send(raw);
|
|
1397
2306
|
} else {
|
|
@@ -1411,7 +2320,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1411
2320
|
return;
|
|
1412
2321
|
}
|
|
1413
2322
|
if (!clientWs.clientId) {
|
|
1414
|
-
clientWs.clientId = `anon-${
|
|
2323
|
+
clientWs.clientId = `anon-${nanoid3(10)}`;
|
|
1415
2324
|
}
|
|
1416
2325
|
const bound = registry.bindClientById(clientWs.clientId, msg.proxyId, clientWs);
|
|
1417
2326
|
if (!bound) {
|
|
@@ -1475,11 +2384,20 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1475
2384
|
}
|
|
1476
2385
|
|
|
1477
2386
|
// src/heartbeat.ts
|
|
2387
|
+
function markAlive(ws) {
|
|
2388
|
+
ws.isAlive = true;
|
|
2389
|
+
}
|
|
1478
2390
|
function setupHeartbeat(wss, interval = 3e4) {
|
|
2391
|
+
wss.on("connection", (ws) => {
|
|
2392
|
+
markAlive(ws);
|
|
2393
|
+
ws.on("pong", () => {
|
|
2394
|
+
markAlive(ws);
|
|
2395
|
+
});
|
|
2396
|
+
});
|
|
1479
2397
|
return setInterval(() => {
|
|
1480
2398
|
for (const ws of wss.clients) {
|
|
1481
2399
|
const sock = ws;
|
|
1482
|
-
if (
|
|
2400
|
+
if (sock.isAlive === false) {
|
|
1483
2401
|
sock.terminate();
|
|
1484
2402
|
continue;
|
|
1485
2403
|
}
|
|
@@ -1490,7 +2408,7 @@ function setupHeartbeat(wss, interval = 3e4) {
|
|
|
1490
2408
|
}
|
|
1491
2409
|
|
|
1492
2410
|
// src/chaos.ts
|
|
1493
|
-
import { WebSocket as
|
|
2411
|
+
import { WebSocket as WebSocket7 } from "ws";
|
|
1494
2412
|
function parseRelayChaosFromEnv(env) {
|
|
1495
2413
|
const enabled = env.DEV_ANYWHERE_RELAY_CHAOS === "1";
|
|
1496
2414
|
const types = env.DEV_ANYWHERE_RELAY_CHAOS_TYPES?.split(",").map((type) => type.trim()).filter(Boolean);
|
|
@@ -1512,7 +2430,7 @@ function createRelayChaos(options, logger) {
|
|
|
1512
2430
|
return true;
|
|
1513
2431
|
}
|
|
1514
2432
|
function sendNow(ws, data) {
|
|
1515
|
-
if (ws.readyState ===
|
|
2433
|
+
if (ws.readyState === WebSocket7.OPEN) {
|
|
1516
2434
|
ws.send(data);
|
|
1517
2435
|
}
|
|
1518
2436
|
}
|
|
@@ -1537,8 +2455,495 @@ function createRelayChaos(options, logger) {
|
|
|
1537
2455
|
};
|
|
1538
2456
|
}
|
|
1539
2457
|
|
|
2458
|
+
// src/voice/config-store.ts
|
|
2459
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
2460
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
2461
|
+
var DEFAULT_STORED_CONFIG = {
|
|
2462
|
+
provider: "aliyun-bailian",
|
|
2463
|
+
region: "cn",
|
|
2464
|
+
asrModel: "qwen3-asr-flash-realtime",
|
|
2465
|
+
ttsModel: "cosyvoice-v3-flash",
|
|
2466
|
+
ttsVoice: "longanyang",
|
|
2467
|
+
turnIdleSeconds: 3
|
|
2468
|
+
};
|
|
2469
|
+
function redacted(config) {
|
|
2470
|
+
return VoiceProviderConfigSchema.parse({
|
|
2471
|
+
provider: config.provider,
|
|
2472
|
+
configured: Boolean(config.apiKey),
|
|
2473
|
+
region: config.region,
|
|
2474
|
+
asrModel: config.asrModel,
|
|
2475
|
+
ttsModel: config.ttsModel,
|
|
2476
|
+
ttsVoice: config.ttsVoice,
|
|
2477
|
+
turnIdleSeconds: config.turnIdleSeconds
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
function mergeDefaults(defaults) {
|
|
2481
|
+
return {
|
|
2482
|
+
...DEFAULT_STORED_CONFIG,
|
|
2483
|
+
...defaults
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
function parseStoredConfig(raw, fallback) {
|
|
2487
|
+
if (!raw || typeof raw !== "object") return fallback;
|
|
2488
|
+
const candidate = raw;
|
|
2489
|
+
return {
|
|
2490
|
+
...fallback,
|
|
2491
|
+
provider: "aliyun-bailian",
|
|
2492
|
+
...typeof candidate.apiKey === "string" && candidate.apiKey.length > 0 ? { apiKey: candidate.apiKey } : { apiKey: void 0 },
|
|
2493
|
+
...candidate.region === "cn" || candidate.region === "intl" ? { region: candidate.region } : {},
|
|
2494
|
+
...typeof candidate.asrModel === "string" && candidate.asrModel.length > 0 ? { asrModel: candidate.asrModel } : {},
|
|
2495
|
+
...typeof candidate.ttsModel === "string" && candidate.ttsModel.length > 0 ? { ttsModel: candidate.ttsModel } : {},
|
|
2496
|
+
...typeof candidate.ttsVoice === "string" && candidate.ttsVoice.length > 0 ? { ttsVoice: candidate.ttsVoice } : {},
|
|
2497
|
+
...typeof candidate.turnIdleSeconds === "number" && Number.isSafeInteger(candidate.turnIdleSeconds) && candidate.turnIdleSeconds > 0 ? { turnIdleSeconds: candidate.turnIdleSeconds } : {}
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function createVoiceConfigStore(options = {}) {
|
|
2501
|
+
const fallback = mergeDefaults(options.defaults);
|
|
2502
|
+
const filePath = options.dataDir ? join2(options.dataDir, "voice-config.json") : null;
|
|
2503
|
+
let memoryConfig = fallback;
|
|
2504
|
+
function load() {
|
|
2505
|
+
if (!filePath) return memoryConfig;
|
|
2506
|
+
if (!existsSync(filePath)) return fallback;
|
|
2507
|
+
try {
|
|
2508
|
+
return parseStoredConfig(JSON.parse(readFileSync2(filePath, "utf8")), fallback);
|
|
2509
|
+
} catch {
|
|
2510
|
+
return fallback;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
function save(config) {
|
|
2514
|
+
if (!filePath) {
|
|
2515
|
+
memoryConfig = config;
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
mkdirSync(dirname2(filePath), { recursive: true });
|
|
2519
|
+
writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
|
|
2520
|
+
`, { mode: 384 });
|
|
2521
|
+
}
|
|
2522
|
+
return {
|
|
2523
|
+
read() {
|
|
2524
|
+
return redacted(load());
|
|
2525
|
+
},
|
|
2526
|
+
update(update) {
|
|
2527
|
+
const parsed = VoiceConfigUpdateSchema.parse(update);
|
|
2528
|
+
const current = load();
|
|
2529
|
+
const next = {
|
|
2530
|
+
...current,
|
|
2531
|
+
provider: "aliyun-bailian",
|
|
2532
|
+
...parsed.clearApiKey ? { apiKey: void 0 } : {},
|
|
2533
|
+
...parsed.apiKey ? { apiKey: parsed.apiKey } : {},
|
|
2534
|
+
...parsed.region ? { region: parsed.region } : {},
|
|
2535
|
+
...parsed.asrModel ? { asrModel: parsed.asrModel } : {},
|
|
2536
|
+
...parsed.ttsModel ? { ttsModel: parsed.ttsModel } : {},
|
|
2537
|
+
...parsed.ttsVoice ? { ttsVoice: parsed.ttsVoice } : {},
|
|
2538
|
+
...parsed.turnIdleSeconds ? { turnIdleSeconds: parsed.turnIdleSeconds } : {}
|
|
2539
|
+
};
|
|
2540
|
+
save(next);
|
|
2541
|
+
return redacted(next);
|
|
2542
|
+
},
|
|
2543
|
+
readSecret() {
|
|
2544
|
+
return load();
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// src/voice/asr-ws.ts
|
|
2550
|
+
import { WebSocket as WebSocket8 } from "ws";
|
|
2551
|
+
function sendJson(ws, payload) {
|
|
2552
|
+
if (ws.readyState === WebSocket8.OPEN) {
|
|
2553
|
+
ws.send(JSON.stringify(payload));
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function toBuffer(data) {
|
|
2557
|
+
if (Buffer.isBuffer(data)) return data;
|
|
2558
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
2559
|
+
return Buffer.concat(data);
|
|
2560
|
+
}
|
|
2561
|
+
function parseJson(data) {
|
|
2562
|
+
try {
|
|
2563
|
+
return JSON.parse(toBuffer(data).toString("utf8"));
|
|
2564
|
+
} catch {
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
function isStartMessage(payload) {
|
|
2569
|
+
if (!payload || typeof payload !== "object") return false;
|
|
2570
|
+
const record = payload;
|
|
2571
|
+
return record.type === "start" && typeof record.sessionId === "string";
|
|
2572
|
+
}
|
|
2573
|
+
function handleVoiceAsrConnection(ws, store, logger, providers) {
|
|
2574
|
+
let provider = null;
|
|
2575
|
+
function start(payload) {
|
|
2576
|
+
const config = store.readSecret();
|
|
2577
|
+
if (!config.apiKey) {
|
|
2578
|
+
sendJson(ws, {
|
|
2579
|
+
type: "error",
|
|
2580
|
+
errorCode: "not_configured",
|
|
2581
|
+
error: "Voice provider is not configured"
|
|
2582
|
+
});
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
provider?.close();
|
|
2586
|
+
provider = providers.current(config).createAsrClient(config, {
|
|
2587
|
+
sampleRate: payload.sampleRate ?? 16e3,
|
|
2588
|
+
language: "zh"
|
|
2589
|
+
});
|
|
2590
|
+
provider.on("ready", () => sendJson(ws, { type: "ready" }));
|
|
2591
|
+
provider.on("partial", (text) => sendJson(ws, { type: "partial", text }));
|
|
2592
|
+
provider.on("final", (text) => sendJson(ws, { type: "final", text }));
|
|
2593
|
+
provider.on(
|
|
2594
|
+
"error",
|
|
2595
|
+
(error) => sendJson(ws, { type: "error", error: error.message || "ASR failed" })
|
|
2596
|
+
);
|
|
2597
|
+
provider.on("closed", (code, reason) => sendJson(ws, { type: "closed", code, reason }));
|
|
2598
|
+
}
|
|
2599
|
+
ws.on("message", (data, isBinary) => {
|
|
2600
|
+
if (isBinary) {
|
|
2601
|
+
provider?.sendPcm(toBuffer(data));
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
const payload = parseJson(data);
|
|
2605
|
+
if (isStartMessage(payload)) {
|
|
2606
|
+
start(payload);
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
if (payload && typeof payload === "object" && payload.type === "stop") {
|
|
2610
|
+
provider?.stop();
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
ws.on("close", () => {
|
|
2614
|
+
provider?.close();
|
|
2615
|
+
provider = null;
|
|
2616
|
+
});
|
|
2617
|
+
ws.on("error", (err) => {
|
|
2618
|
+
logger.warn({ err }, "Voice ASR websocket error");
|
|
2619
|
+
provider?.close();
|
|
2620
|
+
provider = null;
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// src/voice/tts-ws.ts
|
|
2625
|
+
import { WebSocket as WebSocket9 } from "ws";
|
|
2626
|
+
function sendJson2(ws, payload) {
|
|
2627
|
+
if (ws.readyState === WebSocket9.OPEN) {
|
|
2628
|
+
ws.send(JSON.stringify(payload));
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
function parseJson2(data) {
|
|
2632
|
+
try {
|
|
2633
|
+
const buffer = Buffer.isBuffer(data) ? data : data instanceof ArrayBuffer ? Buffer.from(data) : Buffer.concat(data);
|
|
2634
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
2635
|
+
} catch {
|
|
2636
|
+
return null;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
function isSpeakMessage(payload) {
|
|
2640
|
+
if (!payload || typeof payload !== "object") return false;
|
|
2641
|
+
const record = payload;
|
|
2642
|
+
return record.type === "speak" && typeof record.requestId === "string" && typeof record.text === "string" && record.text.length > 0;
|
|
2643
|
+
}
|
|
2644
|
+
function nowMs() {
|
|
2645
|
+
return Date.now();
|
|
2646
|
+
}
|
|
2647
|
+
function buildStats(requestId, text, config) {
|
|
2648
|
+
return {
|
|
2649
|
+
requestId,
|
|
2650
|
+
textChars: text.length,
|
|
2651
|
+
provider: config.provider,
|
|
2652
|
+
region: config.region,
|
|
2653
|
+
ttsModel: config.ttsModel,
|
|
2654
|
+
ttsVoice: config.ttsVoice,
|
|
2655
|
+
startedAt: nowMs(),
|
|
2656
|
+
firstAudioAt: null,
|
|
2657
|
+
audioBytes: 0,
|
|
2658
|
+
audioChunks: 0
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
function statsLogFields(stats) {
|
|
2662
|
+
const currentTime = nowMs();
|
|
2663
|
+
return {
|
|
2664
|
+
requestId: stats.requestId,
|
|
2665
|
+
textChars: stats.textChars,
|
|
2666
|
+
provider: stats.provider,
|
|
2667
|
+
region: stats.region,
|
|
2668
|
+
ttsModel: stats.ttsModel,
|
|
2669
|
+
ttsVoice: stats.ttsVoice,
|
|
2670
|
+
audioBytes: stats.audioBytes,
|
|
2671
|
+
audioChunks: stats.audioChunks,
|
|
2672
|
+
durationMs: currentTime - stats.startedAt,
|
|
2673
|
+
firstAudioMs: stats.firstAudioAt === null ? null : stats.firstAudioAt - stats.startedAt
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
function closeReasonText(reason) {
|
|
2677
|
+
if (Buffer.isBuffer(reason)) return reason.toString("utf8");
|
|
2678
|
+
return typeof reason === "string" ? reason : "";
|
|
2679
|
+
}
|
|
2680
|
+
function handleVoiceTtsConnection(ws, store, logger, providers) {
|
|
2681
|
+
let provider = null;
|
|
2682
|
+
let providerConfig = null;
|
|
2683
|
+
let activeRequestId = null;
|
|
2684
|
+
let activeStats = null;
|
|
2685
|
+
function ensureProvider() {
|
|
2686
|
+
if (provider && providerConfig) return { client: provider, config: providerConfig };
|
|
2687
|
+
const config = store.readSecret();
|
|
2688
|
+
if (!config.apiKey) {
|
|
2689
|
+
sendJson2(ws, {
|
|
2690
|
+
type: "error",
|
|
2691
|
+
errorCode: "not_configured",
|
|
2692
|
+
error: "Voice provider is not configured"
|
|
2693
|
+
});
|
|
2694
|
+
return null;
|
|
2695
|
+
}
|
|
2696
|
+
provider = providers.current(config).createTtsClient(config, {
|
|
2697
|
+
sampleRate: 24e3
|
|
2698
|
+
});
|
|
2699
|
+
providerConfig = config;
|
|
2700
|
+
provider.on("started", () => {
|
|
2701
|
+
if (activeStats) logger.info(statsLogFields(activeStats), "Voice TTS started");
|
|
2702
|
+
sendJson2(ws, { type: "started", requestId: activeRequestId });
|
|
2703
|
+
});
|
|
2704
|
+
provider.on("audio", (chunk) => {
|
|
2705
|
+
if (activeStats) {
|
|
2706
|
+
if (activeStats.firstAudioAt === null) activeStats.firstAudioAt = nowMs();
|
|
2707
|
+
activeStats.audioBytes += chunk.byteLength;
|
|
2708
|
+
activeStats.audioChunks += 1;
|
|
2709
|
+
}
|
|
2710
|
+
if (ws.readyState === WebSocket9.OPEN) ws.send(chunk);
|
|
2711
|
+
});
|
|
2712
|
+
provider.on("finished", () => {
|
|
2713
|
+
if (activeStats) logger.info(statsLogFields(activeStats), "Voice TTS finished");
|
|
2714
|
+
sendJson2(ws, { type: "finished", requestId: activeRequestId });
|
|
2715
|
+
activeRequestId = null;
|
|
2716
|
+
activeStats = null;
|
|
2717
|
+
});
|
|
2718
|
+
provider.on("error", (error) => {
|
|
2719
|
+
if (activeStats) {
|
|
2720
|
+
logger.warn(
|
|
2721
|
+
{ ...statsLogFields(activeStats), err: error },
|
|
2722
|
+
"Voice TTS provider reported an error"
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
sendJson2(ws, {
|
|
2726
|
+
type: "error",
|
|
2727
|
+
requestId: activeRequestId,
|
|
2728
|
+
error: error.message || "TTS failed"
|
|
2729
|
+
});
|
|
2730
|
+
activeRequestId = null;
|
|
2731
|
+
activeStats = null;
|
|
2732
|
+
});
|
|
2733
|
+
provider.on("closed", (code, reason) => {
|
|
2734
|
+
if (activeStats) {
|
|
2735
|
+
logger.warn(
|
|
2736
|
+
{ ...statsLogFields(activeStats), code, reason },
|
|
2737
|
+
"Voice TTS provider closed before finishing"
|
|
2738
|
+
);
|
|
2739
|
+
sendJson2(ws, {
|
|
2740
|
+
type: "error",
|
|
2741
|
+
requestId: activeRequestId,
|
|
2742
|
+
errorCode: "provider_closed",
|
|
2743
|
+
error: "Voice TTS provider closed before finishing"
|
|
2744
|
+
});
|
|
2745
|
+
activeRequestId = null;
|
|
2746
|
+
activeStats = null;
|
|
2747
|
+
} else {
|
|
2748
|
+
logger.info({ code, reason }, "Voice TTS provider closed");
|
|
2749
|
+
provider = null;
|
|
2750
|
+
providerConfig = null;
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
sendJson2(ws, { type: "closed", code, reason });
|
|
2754
|
+
provider = null;
|
|
2755
|
+
providerConfig = null;
|
|
2756
|
+
});
|
|
2757
|
+
return { client: provider, config };
|
|
2758
|
+
}
|
|
2759
|
+
ws.on("message", (data) => {
|
|
2760
|
+
const payload = parseJson2(data);
|
|
2761
|
+
if (!isSpeakMessage(payload)) return;
|
|
2762
|
+
if (activeRequestId) {
|
|
2763
|
+
sendJson2(ws, {
|
|
2764
|
+
type: "error",
|
|
2765
|
+
requestId: payload.requestId,
|
|
2766
|
+
errorCode: "busy",
|
|
2767
|
+
error: "Voice TTS is already speaking"
|
|
2768
|
+
});
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const ensured = ensureProvider();
|
|
2772
|
+
if (!ensured) return;
|
|
2773
|
+
const { client, config } = ensured;
|
|
2774
|
+
activeRequestId = payload.requestId;
|
|
2775
|
+
activeStats = buildStats(payload.requestId, payload.text, config);
|
|
2776
|
+
logger.info(statsLogFields(activeStats), "Voice TTS request received");
|
|
2777
|
+
try {
|
|
2778
|
+
client.speak(payload.text);
|
|
2779
|
+
} catch (err) {
|
|
2780
|
+
if (activeStats) {
|
|
2781
|
+
logger.warn(
|
|
2782
|
+
{ ...statsLogFields(activeStats), err },
|
|
2783
|
+
"Voice TTS request failed before provider accepted it"
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
sendJson2(ws, {
|
|
2787
|
+
type: "error",
|
|
2788
|
+
requestId: payload.requestId,
|
|
2789
|
+
error: err instanceof Error ? err.message : "Voice TTS failed"
|
|
2790
|
+
});
|
|
2791
|
+
activeRequestId = null;
|
|
2792
|
+
activeStats = null;
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
ws.on("close", (code, reason) => {
|
|
2796
|
+
if (activeStats) {
|
|
2797
|
+
logger.warn(
|
|
2798
|
+
{ ...statsLogFields(activeStats), code, reason: closeReasonText(reason) },
|
|
2799
|
+
"Voice TTS client websocket closed before finishing"
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
const currentProvider = provider;
|
|
2803
|
+
provider = null;
|
|
2804
|
+
providerConfig = null;
|
|
2805
|
+
activeRequestId = null;
|
|
2806
|
+
activeStats = null;
|
|
2807
|
+
currentProvider?.close();
|
|
2808
|
+
});
|
|
2809
|
+
ws.on("error", (err) => {
|
|
2810
|
+
logger.warn({ err }, "Voice TTS websocket error");
|
|
2811
|
+
provider?.close();
|
|
2812
|
+
provider = null;
|
|
2813
|
+
providerConfig = null;
|
|
2814
|
+
activeRequestId = null;
|
|
2815
|
+
activeStats = null;
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
// src/voice/capabilities.ts
|
|
2820
|
+
var CUSTOMIZATION_ENDPOINTS = {
|
|
2821
|
+
cn: "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization",
|
|
2822
|
+
intl: "https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization"
|
|
2823
|
+
};
|
|
2824
|
+
function modelFromCustomCosyVoiceId(voiceId) {
|
|
2825
|
+
const match = voiceId.match(/^(cosyvoice-v3(?:\.5)?-(?:flash|plus))-/);
|
|
2826
|
+
return match?.[1];
|
|
2827
|
+
}
|
|
2828
|
+
async function fetchCustomVoices(fetchImpl, config) {
|
|
2829
|
+
if (!config.apiKey) return [];
|
|
2830
|
+
const response = await fetchImpl(CUSTOMIZATION_ENDPOINTS[config.region], {
|
|
2831
|
+
method: "POST",
|
|
2832
|
+
headers: {
|
|
2833
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
2834
|
+
"Content-Type": "application/json"
|
|
2835
|
+
},
|
|
2836
|
+
body: JSON.stringify({
|
|
2837
|
+
model: "voice-enrollment",
|
|
2838
|
+
input: {
|
|
2839
|
+
action: "list_voice",
|
|
2840
|
+
page_size: 100,
|
|
2841
|
+
page_index: 0
|
|
2842
|
+
}
|
|
2843
|
+
})
|
|
2844
|
+
});
|
|
2845
|
+
if (!response.ok) return [];
|
|
2846
|
+
const payload = response.json();
|
|
2847
|
+
const voiceList = (await payload).output?.voice_list ?? [];
|
|
2848
|
+
return voiceList.filter((voice) => !voice.status || voice.status === "OK").flatMap((voice) => {
|
|
2849
|
+
if (!voice.voice_id) return [];
|
|
2850
|
+
const model = modelFromCustomCosyVoiceId(voice.voice_id);
|
|
2851
|
+
return [
|
|
2852
|
+
{
|
|
2853
|
+
value: voice.voice_id,
|
|
2854
|
+
label: [voice.voice_id, "\u81EA\u5B9A\u4E49", voice.voice_prompt].filter(Boolean).join(" \xB7 "),
|
|
2855
|
+
...model ? { model } : {},
|
|
2856
|
+
source: "custom"
|
|
2857
|
+
}
|
|
2858
|
+
];
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
function createBailianVoiceCapabilitiesProvider(options = {}) {
|
|
2862
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
2863
|
+
const now = options.now ?? Date.now;
|
|
2864
|
+
return {
|
|
2865
|
+
async read(config) {
|
|
2866
|
+
let customVoices;
|
|
2867
|
+
try {
|
|
2868
|
+
customVoices = await fetchCustomVoices(fetchImpl, config);
|
|
2869
|
+
} catch {
|
|
2870
|
+
customVoices = [];
|
|
2871
|
+
}
|
|
2872
|
+
const bundled = createBundledBailianVoiceCapabilities(now());
|
|
2873
|
+
return {
|
|
2874
|
+
...bundled,
|
|
2875
|
+
ttsVoices: [...bundled.ttsVoices, ...customVoices]
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// src/voice/bailian-provider.ts
|
|
2882
|
+
function requireApiKey(config) {
|
|
2883
|
+
if (!config.apiKey) throw new Error("Voice provider is not configured");
|
|
2884
|
+
return config.apiKey;
|
|
2885
|
+
}
|
|
2886
|
+
function createBailianVoiceProvider(options = {}) {
|
|
2887
|
+
const asrClientFactory = options.asrClientFactory ?? createBailianAsrClient;
|
|
2888
|
+
const ttsClientFactory = options.ttsClientFactory ?? createBailianTtsClient;
|
|
2889
|
+
const capabilitiesProvider = options.capabilitiesProvider ?? createBailianVoiceCapabilitiesProvider();
|
|
2890
|
+
const configTester = options.configTester ?? createBailianVoiceConfigTester({
|
|
2891
|
+
asrClientFactory,
|
|
2892
|
+
ttsClientFactory
|
|
2893
|
+
});
|
|
2894
|
+
return {
|
|
2895
|
+
id: "aliyun-bailian",
|
|
2896
|
+
createAsrClient(config, clientOptions) {
|
|
2897
|
+
return asrClientFactory({
|
|
2898
|
+
apiKey: requireApiKey(config),
|
|
2899
|
+
region: config.region,
|
|
2900
|
+
model: config.asrModel,
|
|
2901
|
+
sampleRate: clientOptions.sampleRate,
|
|
2902
|
+
language: clientOptions.language
|
|
2903
|
+
});
|
|
2904
|
+
},
|
|
2905
|
+
createTtsClient(config, clientOptions) {
|
|
2906
|
+
return ttsClientFactory({
|
|
2907
|
+
apiKey: requireApiKey(config),
|
|
2908
|
+
region: config.region,
|
|
2909
|
+
model: config.ttsModel,
|
|
2910
|
+
voice: config.ttsVoice,
|
|
2911
|
+
sampleRate: clientOptions.sampleRate
|
|
2912
|
+
});
|
|
2913
|
+
},
|
|
2914
|
+
readCapabilities(config) {
|
|
2915
|
+
return capabilitiesProvider.read(config);
|
|
2916
|
+
},
|
|
2917
|
+
testConfig(config) {
|
|
2918
|
+
return configTester.test(config);
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// src/voice/provider.ts
|
|
2924
|
+
function createVoiceProviderRegistry(adapters) {
|
|
2925
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2926
|
+
for (const adapter of adapters) {
|
|
2927
|
+
if (byId.has(adapter.id)) {
|
|
2928
|
+
throw new Error(`Duplicate voice provider: ${adapter.id}`);
|
|
2929
|
+
}
|
|
2930
|
+
byId.set(adapter.id, adapter);
|
|
2931
|
+
}
|
|
2932
|
+
function require2(providerId) {
|
|
2933
|
+
const adapter = byId.get(providerId);
|
|
2934
|
+
if (!adapter) throw new Error(`Unsupported voice provider: ${providerId}`);
|
|
2935
|
+
return adapter;
|
|
2936
|
+
}
|
|
2937
|
+
return {
|
|
2938
|
+
current(config) {
|
|
2939
|
+
return require2(config.provider);
|
|
2940
|
+
},
|
|
2941
|
+
require: require2
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
|
|
1540
2945
|
// src/server.ts
|
|
1541
|
-
var MODULE_DIR =
|
|
2946
|
+
var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
|
|
1542
2947
|
var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
|
|
1543
2948
|
function createRelayServer(options) {
|
|
1544
2949
|
const { heartbeatInterval = 3e4, logger, dataDir, proxyToken, clientToken, chaos } = options;
|
|
@@ -1560,6 +2965,18 @@ function createRelayServer(options) {
|
|
|
1560
2965
|
);
|
|
1561
2966
|
}
|
|
1562
2967
|
const registry = new RelayRegistry();
|
|
2968
|
+
const voiceConfigStore = createVoiceConfigStore({
|
|
2969
|
+
dataDir,
|
|
2970
|
+
defaults: options.voiceDefaults
|
|
2971
|
+
});
|
|
2972
|
+
const voiceProviders = options.voiceProviderRegistry ?? createVoiceProviderRegistry([
|
|
2973
|
+
createBailianVoiceProvider({
|
|
2974
|
+
asrClientFactory: options.voiceAsrClientFactory,
|
|
2975
|
+
ttsClientFactory: options.voiceTtsClientFactory,
|
|
2976
|
+
capabilitiesProvider: options.voiceCapabilitiesProvider,
|
|
2977
|
+
configTester: options.voiceConfigTester
|
|
2978
|
+
})
|
|
2979
|
+
]);
|
|
1563
2980
|
const relayChaos = chaos?.enabled ? createRelayChaos(chaos, logger) : void 0;
|
|
1564
2981
|
if (chaos?.enabled) {
|
|
1565
2982
|
logger.warn(
|
|
@@ -1586,7 +3003,7 @@ function createRelayServer(options) {
|
|
|
1586
3003
|
immutable: true
|
|
1587
3004
|
})
|
|
1588
3005
|
);
|
|
1589
|
-
if (
|
|
3006
|
+
if (existsSync2(fontAssetDir)) {
|
|
1590
3007
|
app.use(
|
|
1591
3008
|
"/fonts",
|
|
1592
3009
|
express.static(fontAssetDir, {
|
|
@@ -1607,6 +3024,8 @@ function createRelayServer(options) {
|
|
|
1607
3024
|
const httpServer = createServer(app);
|
|
1608
3025
|
const proxyWss = new WebSocketServer({ noServer: true });
|
|
1609
3026
|
const clientWss = new WebSocketServer({ noServer: true });
|
|
3027
|
+
const voiceAsrWss = new WebSocketServer({ noServer: true });
|
|
3028
|
+
const voiceTtsWss = new WebSocketServer({ noServer: true });
|
|
1610
3029
|
httpServer.on("upgrade", (request, socket, head) => {
|
|
1611
3030
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
1612
3031
|
const { pathname } = url;
|
|
@@ -1638,22 +3057,32 @@ function createRelayServer(options) {
|
|
|
1638
3057
|
});
|
|
1639
3058
|
return;
|
|
1640
3059
|
}
|
|
1641
|
-
if (pathname === "/client") {
|
|
3060
|
+
if (pathname === "/client" || pathname === "/voice/asr" || pathname === "/voice/tts") {
|
|
1642
3061
|
if (clientTokenRequired) {
|
|
1643
3062
|
const token = url.searchParams.get("token");
|
|
1644
3063
|
if (token !== clientToken) {
|
|
1645
3064
|
logger.warn(
|
|
1646
|
-
{ ip: request.socket.remoteAddress },
|
|
1647
|
-
"rejected
|
|
3065
|
+
{ ip: request.socket.remoteAddress, pathname },
|
|
3066
|
+
"rejected client-side upgrade: invalid token"
|
|
1648
3067
|
);
|
|
1649
3068
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1650
3069
|
socket.destroy();
|
|
1651
3070
|
return;
|
|
1652
3071
|
}
|
|
1653
3072
|
}
|
|
1654
|
-
|
|
1655
|
-
clientWss.
|
|
1656
|
-
|
|
3073
|
+
if (pathname === "/client") {
|
|
3074
|
+
clientWss.handleUpgrade(request, socket, head, (ws) => {
|
|
3075
|
+
clientWss.emit("connection", ws, request);
|
|
3076
|
+
});
|
|
3077
|
+
} else if (pathname === "/voice/asr") {
|
|
3078
|
+
voiceAsrWss.handleUpgrade(request, socket, head, (ws) => {
|
|
3079
|
+
voiceAsrWss.emit("connection", ws, request);
|
|
3080
|
+
});
|
|
3081
|
+
} else {
|
|
3082
|
+
voiceTtsWss.handleUpgrade(request, socket, head, (ws) => {
|
|
3083
|
+
voiceTtsWss.emit("connection", ws, request);
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
1657
3086
|
return;
|
|
1658
3087
|
}
|
|
1659
3088
|
socket.destroy();
|
|
@@ -1662,31 +3091,61 @@ function createRelayServer(options) {
|
|
|
1662
3091
|
handleProxyConnection(ws, registry, logger, relayChaos);
|
|
1663
3092
|
});
|
|
1664
3093
|
clientWss.on("connection", (ws) => {
|
|
1665
|
-
handleClientConnection(ws, registry, logger, relayChaos);
|
|
3094
|
+
handleClientConnection(ws, registry, logger, relayChaos, voiceConfigStore, voiceProviders);
|
|
3095
|
+
});
|
|
3096
|
+
voiceAsrWss.on("connection", (ws) => {
|
|
3097
|
+
handleVoiceAsrConnection(ws, voiceConfigStore, logger, voiceProviders);
|
|
3098
|
+
});
|
|
3099
|
+
voiceTtsWss.on("connection", (ws) => {
|
|
3100
|
+
handleVoiceTtsConnection(ws, voiceConfigStore, logger, voiceProviders);
|
|
1666
3101
|
});
|
|
1667
3102
|
const proxyHeartbeat = setupHeartbeat(proxyWss, heartbeatInterval);
|
|
1668
3103
|
const clientHeartbeat = setupHeartbeat(clientWss, heartbeatInterval);
|
|
3104
|
+
const voiceAsrHeartbeat = setupHeartbeat(voiceAsrWss, heartbeatInterval);
|
|
3105
|
+
const voiceTtsHeartbeat = setupHeartbeat(voiceTtsWss, heartbeatInterval);
|
|
1669
3106
|
async function close() {
|
|
1670
3107
|
clearInterval(proxyHeartbeat);
|
|
1671
3108
|
clearInterval(clientHeartbeat);
|
|
3109
|
+
clearInterval(voiceAsrHeartbeat);
|
|
3110
|
+
clearInterval(voiceTtsHeartbeat);
|
|
1672
3111
|
for (const ws of proxyWss.clients) {
|
|
1673
3112
|
ws.terminate();
|
|
1674
3113
|
}
|
|
1675
3114
|
for (const ws of clientWss.clients) {
|
|
1676
3115
|
ws.terminate();
|
|
1677
3116
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
1684
|
-
await
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
3117
|
+
for (const ws of voiceAsrWss.clients) {
|
|
3118
|
+
ws.terminate();
|
|
3119
|
+
}
|
|
3120
|
+
for (const ws of voiceTtsWss.clients) {
|
|
3121
|
+
ws.terminate();
|
|
3122
|
+
}
|
|
3123
|
+
await Promise.all([
|
|
3124
|
+
new Promise((resolve2, reject) => {
|
|
3125
|
+
proxyWss.close((err) => {
|
|
3126
|
+
if (err) reject(err);
|
|
3127
|
+
else resolve2();
|
|
3128
|
+
});
|
|
3129
|
+
}),
|
|
3130
|
+
new Promise((resolve2, reject) => {
|
|
3131
|
+
clientWss.close((err) => {
|
|
3132
|
+
if (err) reject(err);
|
|
3133
|
+
else resolve2();
|
|
3134
|
+
});
|
|
3135
|
+
}),
|
|
3136
|
+
new Promise((resolve2, reject) => {
|
|
3137
|
+
voiceAsrWss.close((err) => {
|
|
3138
|
+
if (err) reject(err);
|
|
3139
|
+
else resolve2();
|
|
3140
|
+
});
|
|
3141
|
+
}),
|
|
3142
|
+
new Promise((resolve2, reject) => {
|
|
3143
|
+
voiceTtsWss.close((err) => {
|
|
3144
|
+
if (err) reject(err);
|
|
3145
|
+
else resolve2();
|
|
3146
|
+
});
|
|
3147
|
+
})
|
|
3148
|
+
]);
|
|
1690
3149
|
await new Promise((resolve2, reject) => {
|
|
1691
3150
|
httpServer.close((err) => {
|
|
1692
3151
|
if (err) reject(err);
|
|
@@ -1702,4 +3161,4 @@ export {
|
|
|
1702
3161
|
parseRelayChaosFromEnv,
|
|
1703
3162
|
createRelayServer
|
|
1704
3163
|
};
|
|
1705
|
-
//# sourceMappingURL=chunk-
|
|
3164
|
+
//# sourceMappingURL=chunk-ERH2EO6I.js.map
|