@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.
Files changed (36) hide show
  1. package/dist/{chunk-DFVUNUQH.js → chunk-ERH2EO6I.js} +1612 -153
  2. package/dist/chunk-ERH2EO6I.js.map +1 -0
  3. package/dist/handlers/client.d.ts +3 -1
  4. package/dist/handlers/client.d.ts.map +1 -1
  5. package/dist/heartbeat.d.ts.map +1 -1
  6. package/dist/index.js +16 -3
  7. package/dist/index.js.map +1 -1
  8. package/dist/runtime-env.d.ts +6 -0
  9. package/dist/runtime-env.d.ts.map +1 -1
  10. package/dist/server.d.ts +16 -0
  11. package/dist/server.d.ts.map +1 -1
  12. package/dist/server.js +1 -1
  13. package/dist/voice/asr-ws.d.ts +8 -0
  14. package/dist/voice/asr-ws.d.ts.map +1 -0
  15. package/dist/voice/bailian-asr.d.ts +35 -0
  16. package/dist/voice/bailian-asr.d.ts.map +1 -0
  17. package/dist/voice/bailian-endpoints.d.ts +4 -0
  18. package/dist/voice/bailian-endpoints.d.ts.map +1 -0
  19. package/dist/voice/bailian-provider.d.ts +13 -0
  20. package/dist/voice/bailian-provider.d.ts.map +1 -0
  21. package/dist/voice/bailian-tts.d.ts +33 -0
  22. package/dist/voice/bailian-tts.d.ts.map +1 -0
  23. package/dist/voice/capabilities.d.ts +21 -0
  24. package/dist/voice/capabilities.d.ts.map +1 -0
  25. package/dist/voice/client-controls.d.ts +7 -0
  26. package/dist/voice/client-controls.d.ts.map +1 -0
  27. package/dist/voice/config-store.d.ts +22 -0
  28. package/dist/voice/config-store.d.ts.map +1 -0
  29. package/dist/voice/config-test.d.ts +22 -0
  30. package/dist/voice/config-test.d.ts.map +1 -0
  31. package/dist/voice/provider.d.ts +41 -0
  32. package/dist/voice/provider.d.ts.map +1 -0
  33. package/dist/voice/tts-ws.d.ts +8 -0
  34. package/dist/voice/tts-ws.d.ts.map +1 -0
  35. package/package.json +2 -2
  36. 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 dirname2, resolve } from "path";
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 z7 } from "zod";
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 = z7.object({
498
+ var ProxyInfoSchema = z8.object({
299
499
  proxyId: IdSchema,
300
- name: z7.string().optional(),
301
- online: z7.boolean(),
302
- sessions: z7.array(z7.string()).optional()
500
+ name: z8.string().optional(),
501
+ online: z8.boolean(),
502
+ sessions: z8.array(z8.string()).optional()
303
503
  });
304
- var AgentCliAvailabilitySchema = z7.object({
305
- available: z7.boolean(),
306
- command: z7.string().optional(),
307
- error: z7.string().optional(),
308
- suggestions: z7.array(z7.string()).optional()
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 = z7.object({
510
+ var AgentCliStatusSchema = z8.object({
311
511
  claude: AgentCliAvailabilitySchema,
312
512
  codex: AgentCliAvailabilitySchema
313
513
  });
314
- var DirEntrySchema = z7.object({ name: z7.string(), isDir: z7.boolean() });
315
- var FileTreeGroupSchema = z7.object({
316
- path: z7.string(),
317
- entries: z7.array(DirEntrySchema)
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 = z7.object({
320
- name: z7.string(),
321
- description: z7.string(),
322
- argumentHint: z7.string().optional(),
323
- source: z7.string()
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 = z7.object({
326
- id: z7.string(),
327
- title: z7.string(),
328
- projectDir: z7.string(),
329
- updatedAt: z7.number(),
330
- provider: z7.enum(providerValues).optional()
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 = z7.object({
333
- role: z7.enum(["user", "assistant"]),
334
- text: z7.string(),
335
- timestamp: z7.number().optional(),
336
- cursor: z7.string().optional()
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 = z7.enum(Object.values(ControlErrorCode));
539
+ var ControlErrorCodeSchema = z8.enum(Object.values(ControlErrorCode));
340
540
  var RequestErrorShape = {
341
- error: z7.string().optional(),
541
+ error: z8.string().optional(),
342
542
  errorCode: ControlErrorCodeSchema.optional()
343
543
  };
344
- var ClipboardImageMimeTypeSchema = z7.enum(["image/png", "image/jpeg", "image/webp", "image/gif"]);
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: z7.object({
350
- type: z7.literal(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: z7.string().optional()
558
+ name: z8.string().optional()
359
559
  }),
360
560
  control("proxy_register_response", {
361
- status: z7.enum(["new", "reconnected"])
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: z7.array(ProxyInfoSchema)
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: z7.boolean(),
571
+ success: z8.boolean(),
372
572
  proxyId: IdSchema.optional(),
373
573
  ...RequestErrorShape
374
574
  }),
375
575
  control("relay_error", {
376
- code: z7.enum(Object.values(RelayErrorCode)),
377
- message: z7.string(),
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: z7.enum(["restored", "proxy_offline", "new"]),
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: z7.string()
645
+ path: z8.string()
407
646
  }, "client_to_proxy"),
408
- control("dir_list_response", { ...RequestIdShape, ...RequestErrorShape, entries: z7.array(DirEntrySchema), path: z7.string() }, "proxy_to_client"),
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: z7.string() }, "client_to_proxy"),
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: z7.string(),
415
- success: z7.boolean()
653
+ path: z8.string(),
654
+ success: z8.boolean()
416
655
  }, "proxy_to_client"),
417
656
  // 命令列表推送,proxy 将可用命令列表推给 client
418
- control("command_list_push", { commands: z7.array(CommandEntrySchema) }, "proxy_to_client"),
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: z7.array(FileTreeGroupSchema)
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: z7.enum(["default", "auto_accept", "plan"]),
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: z7.array(HistorySessionSchema) }, "proxy_to_client"),
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: z7.string() }, "proxy_to_client"),
678
+ control("terminal_title", { sessionId: IdSchema, title: z8.string() }, "proxy_to_client"),
440
679
  // 终端尺寸变化,proxy -> client
441
- control("terminal_resize", { sessionId: IdSchema, cols: z7.number().int().positive(), rows: z7.number().int().positive() }, "proxy_to_client"),
442
- control("terminal_resize_request", { sessionId: IdSchema, cols: z7.number().int().positive(), rows: z7.number().int().positive() }, "client_to_proxy"),
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: z7.string() }, "client_to_proxy"),
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: z7.boolean(),
450
- name: z7.string().optional(),
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: z7.boolean(),
459
- isError: z7.boolean(),
697
+ success: z8.boolean(),
698
+ isError: z8.boolean(),
460
699
  // stream-json result.result 是本轮最终文本。assistant_message 流丢失或 CLI 未发增量时,
461
700
  // Web 用它作为 JSON 模式兜底展示,避免 turn 已结束但界面空白。
462
- result: z7.string().optional()
701
+ result: z8.string().optional()
463
702
  }, "proxy_to_client"),
464
703
  // 客户端发送到 PTY 的原始字节(ANSI 序列),不追加换行
465
- control("remote_input_raw", { sessionId: IdSchema, data: z7.string() }, "client_to_proxy"),
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: z7.string().min(1),
471
- fileName: z7.string().optional()
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: z7.boolean(),
716
+ success: z8.boolean(),
478
717
  // success=false 时 proxy 没有有效 path 可填;保持 optional 以避免占位空字符串通过校验。
479
- path: z7.string().optional()
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: z7.string().min(1)
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: z7.boolean(),
729
+ success: z8.boolean(),
491
730
  // 同 clipboard_image_upload_response:失败时 proxy 不一定有路径。
492
- path: z7.string().optional(),
731
+ path: z8.string().optional(),
493
732
  mimeType: ClipboardImageMimeTypeSchema.optional(),
494
- dataBase64: z7.string().optional(),
495
- size: z7.number().int().nonnegative().optional()
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: z7.string().min(1)
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: z7.boolean(),
509
- path: z7.string().optional(),
510
- mimeType: z7.string().optional(),
511
- dataBase64: z7.string().optional(),
512
- size: z7.number().int().nonnegative().optional()
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: z7.string().min(1),
520
- dataBase64: z7.string().min(1),
521
- fileName: z7.string().min(1)
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: z7.boolean(),
528
- path: z7.string().optional()
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: z7.string(), agentCli: AgentCliStatusSchema }, "proxy_to_client"),
534
- control("agent_cli_config_update", { ...RequestIdShape, provider: z7.enum(providerValues), path: z7.string().min(1) }, "client_to_proxy"),
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: z7.enum(providerValues),
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: z7.string(),
545
- provider: z7.enum(providerValues),
546
- mode: z7.enum(sessionModeValues).optional(),
547
- resumeSessionId: z7.string().optional(),
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: z7.enum(["default", "auto", "acceptEdits", "plan", "bypassPermissions", "dontAsk"]).optional()
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
- mode: z7.enum(sessionModeValues).optional(),
556
- provider: z7.enum(providerValues).optional(),
557
- ptyOwner: z7.enum(ptyOwnerValues).optional(),
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: z7.number().int().min(1).max(200).optional(),
565
- before: z7.string().optional()
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: z7.array(CommandEntrySchema),
574
- groups: z7.array(FileTreeGroupSchema)
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: z7.array(z7.object({ sessionId: IdSchema, payload: AgentStatusPayloadSchema }))
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: z7.enum(["allow", "deny"]),
591
- delivered: z7.boolean(),
592
- message: z7.string().optional()
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: z7.array(z7.object({
839
+ approvals: z8.array(z8.object({
598
840
  requestId: IdSchema,
599
- toolName: z7.string(),
600
- input: z7.record(z7.string(), z7.unknown())
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: z7.string().optional(),
608
- messages: z7.array(SessionHistoryMessageSchema),
609
- hasMore: z7.boolean().optional(),
610
- nextBefore: z7.string().optional()
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: z7.array(z7.object({
616
- id: z7.string(),
617
- mode: z7.enum(sessionModeValues),
618
- provider: z7.enum(providerValues),
619
- ptyOwner: z7.enum(ptyOwnerValues).optional(),
620
- cwd: z7.string().optional(),
621
- name: z7.string().optional(),
622
- nameLocked: z7.boolean().optional(),
623
- state: z7.enum(sessionStateValues)
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: z7.number().int().positive(),
632
- rows: z7.number().int().positive(),
633
- data: z7.string(),
634
- outputSeq: z7.number().int().nonnegative(),
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 = z7.discriminatedUnion("type", relayControlSchemas);
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 WebSocket4 } from "ws";
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 === WebSocket4.OPEN) {
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-${nanoid(10)}`;
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 (!sock.isAlive) {
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 WebSocket5 } from "ws";
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 === WebSocket5.OPEN) {
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 = dirname2(fileURLToPath2(import.meta.url));
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 (existsSync(fontAssetDir)) {
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 /client upgrade: invalid token"
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
- clientWss.handleUpgrade(request, socket, head, (ws) => {
1655
- clientWss.emit("connection", ws, request);
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
- await new Promise((resolve2, reject) => {
1679
- proxyWss.close((err) => {
1680
- if (err) reject(err);
1681
- else resolve2();
1682
- });
1683
- });
1684
- await new Promise((resolve2, reject) => {
1685
- clientWss.close((err) => {
1686
- if (err) reject(err);
1687
- else resolve2();
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-DFVUNUQH.js.map
3164
+ //# sourceMappingURL=chunk-ERH2EO6I.js.map