@dev-anywhere/relay 0.4.84 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +13 -8
  2. package/assets/web/apple-touch-icon-180x180.png +0 -0
  3. package/assets/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  4. package/assets/web/assets/fvad-C44UvSw_.js +1 -0
  5. package/assets/web/assets/fvad-DYxbW2my.wasm +0 -0
  6. package/assets/web/assets/index-CYKs-fya.js +762 -0
  7. package/assets/web/assets/index-CymnUxwB.css +41 -0
  8. package/assets/web/brand-icon.svg +56 -0
  9. package/assets/web/favicon.ico +0 -0
  10. package/assets/web/index.html +53 -0
  11. package/assets/web/manifest.webmanifest +1 -0
  12. package/assets/web/maskable-icon-512x512.png +0 -0
  13. package/assets/web/notification-sw.js +21 -0
  14. package/assets/web/pwa-192x192.png +0 -0
  15. package/assets/web/pwa-512x512.png +0 -0
  16. package/assets/web/pwa-64x64.png +0 -0
  17. package/assets/web/registerSW.js +1 -0
  18. package/assets/web/sw.js +1 -0
  19. package/assets/web/workbox-10a3128e.js +1 -0
  20. package/dist/{chunk-46NK2URF.js → chunk-2L3L5F4J.js} +566 -227
  21. package/dist/chunk-2L3L5F4J.js.map +1 -0
  22. package/dist/index.js +4 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/registry.d.ts +1 -0
  25. package/dist/registry.d.ts.map +1 -1
  26. package/dist/server.d.ts +1 -0
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +1 -1
  29. package/dist/voice/asr-ws.d.ts.map +1 -1
  30. package/dist/voice/bailian-asr.d.ts.map +1 -1
  31. package/dist/voice/config-test.d.ts.map +1 -1
  32. package/dist/voice/pcm-resample.d.ts +2 -0
  33. package/dist/voice/pcm-resample.d.ts.map +1 -0
  34. package/dist/web-app.d.ts +7 -0
  35. package/dist/web-app.d.ts.map +1 -0
  36. package/package.json +6 -3
  37. package/dist/chunk-46NK2URF.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/server.ts
4
- import express from "express";
5
- import { existsSync as existsSync2 } from "fs";
4
+ import express2 from "express";
5
+ import { existsSync as existsSync3 } from "fs";
6
6
  import { createServer } from "http";
7
7
  import { homedir } from "os";
8
8
  import { dirname as dirname3, resolve } from "path";
@@ -468,13 +468,56 @@ var VoiceSummaryReasonSchema = z7.enum([
468
468
  "approval"
469
469
  ]);
470
470
 
471
+ // ../../packages/shared/dist/schemas/voice-stream.js
472
+ import { z as z8 } from "zod";
473
+ var VoiceAsrAudioEncodingSchema = z8.enum(["pcm_s16le", "mulaw"]);
474
+ var VoiceAsrStartMessageSchema = z8.object({
475
+ type: z8.literal("start"),
476
+ sessionId: z8.string().min(1),
477
+ attemptId: z8.string().min(1),
478
+ sampleRate: z8.number().int().positive(),
479
+ encoding: VoiceAsrAudioEncodingSchema
480
+ });
481
+ var VoiceAsrStopMessageSchema = z8.object({
482
+ type: z8.literal("stop"),
483
+ attemptId: z8.string().min(1)
484
+ });
485
+ var VoiceAsrClientMessageSchema = z8.discriminatedUnion("type", [
486
+ VoiceAsrStartMessageSchema,
487
+ VoiceAsrStopMessageSchema
488
+ ]);
489
+ var VoiceAsrServerMessageSchema = z8.discriminatedUnion("type", [
490
+ z8.object({ type: z8.literal("ready"), attemptId: z8.string().min(1) }),
491
+ z8.object({
492
+ type: z8.literal("audio_ack"),
493
+ attemptId: z8.string().min(1),
494
+ encodedBytes: z8.number().int().nonnegative(),
495
+ pcmBytes: z8.number().int().nonnegative(),
496
+ chunks: z8.number().int().nonnegative()
497
+ }),
498
+ z8.object({ type: z8.literal("partial"), attemptId: z8.string().min(1), text: z8.string() }),
499
+ z8.object({ type: z8.literal("final"), attemptId: z8.string().min(1), text: z8.string() }),
500
+ z8.object({
501
+ type: z8.literal("closed"),
502
+ attemptId: z8.string().min(1),
503
+ code: z8.number().int().optional(),
504
+ reason: z8.string().optional()
505
+ }),
506
+ z8.object({
507
+ type: z8.literal("error"),
508
+ attemptId: z8.string().min(1),
509
+ error: z8.string().optional(),
510
+ errorCode: z8.string().optional()
511
+ })
512
+ ]);
513
+
471
514
  // ../../packages/shared/dist/builders/index.js
472
515
  function serializeControl(msg) {
473
516
  return JSON.stringify(msg);
474
517
  }
475
518
 
476
519
  // ../../packages/shared/dist/schemas/relay-control.js
477
- import { z as z8 } from "zod";
520
+ import { z as z9 } from "zod";
478
521
 
479
522
  // ../../packages/shared/dist/constants/relay-errors.js
480
523
  var RelayErrorCode = {
@@ -501,75 +544,75 @@ var ControlErrorCode = {
501
544
  };
502
545
 
503
546
  // ../../packages/shared/dist/schemas/relay-control.js
504
- var ProxyInfoSchema = z8.object({
547
+ var ProxyInfoSchema = z9.object({
505
548
  proxyId: IdSchema,
506
- name: z8.string().optional(),
507
- online: z8.boolean(),
508
- sessions: z8.array(z8.string()).optional()
549
+ name: z9.string().optional(),
550
+ online: z9.boolean(),
551
+ sessions: z9.array(z9.string()).optional()
509
552
  });
510
- var RelayClientInfoSchema = z8.object({
553
+ var RelayClientInfoSchema = z9.object({
511
554
  clientId: IdSchema,
512
555
  proxyId: IdSchema.optional(),
513
- connectedAt: z8.number().int().nonnegative(),
514
- current: z8.boolean().optional(),
515
- userAgent: z8.string().optional(),
516
- platform: z8.string().optional(),
517
- maxTouchPoints: z8.number().int().nonnegative().optional(),
518
- browserName: z8.string().min(1),
519
- osName: z8.string().min(1),
520
- deviceKind: z8.enum(["desktop", "tablet", "phone", "unknown"]),
521
- remoteAddress: z8.string().optional()
556
+ connectedAt: z9.number().int().nonnegative(),
557
+ current: z9.boolean().optional(),
558
+ userAgent: z9.string().optional(),
559
+ platform: z9.string().optional(),
560
+ maxTouchPoints: z9.number().int().nonnegative().optional(),
561
+ browserName: z9.string().min(1),
562
+ osName: z9.string().min(1),
563
+ deviceKind: z9.enum(["desktop", "tablet", "phone", "unknown"]),
564
+ remoteAddress: z9.string().optional()
522
565
  });
523
- var AgentCliAvailabilitySchema = z8.object({
524
- available: z8.boolean(),
525
- command: z8.string().optional(),
526
- error: z8.string().optional(),
527
- suggestions: z8.array(z8.string()).optional()
566
+ var AgentCliAvailabilitySchema = z9.object({
567
+ available: z9.boolean(),
568
+ command: z9.string().optional(),
569
+ error: z9.string().optional(),
570
+ suggestions: z9.array(z9.string()).optional()
528
571
  });
529
- var AgentCliStatusSchema = z8.object({
572
+ var AgentCliStatusSchema = z9.object({
530
573
  claude: AgentCliAvailabilitySchema,
531
574
  codex: AgentCliAvailabilitySchema
532
575
  });
533
- var DirEntrySchema = z8.object({ name: z8.string(), isDir: z8.boolean() });
534
- var FileTreeGroupSchema = z8.object({
535
- path: z8.string(),
536
- entries: z8.array(DirEntrySchema)
576
+ var DirEntrySchema = z9.object({ name: z9.string(), isDir: z9.boolean() });
577
+ var FileTreeGroupSchema = z9.object({
578
+ path: z9.string(),
579
+ entries: z9.array(DirEntrySchema)
537
580
  });
538
- var CommandEntrySchema = z8.object({
539
- name: z8.string(),
540
- description: z8.string(),
541
- argumentHint: z8.string().optional(),
542
- source: z8.string()
581
+ var CommandEntrySchema = z9.object({
582
+ name: z9.string(),
583
+ description: z9.string(),
584
+ argumentHint: z9.string().optional(),
585
+ source: z9.string()
543
586
  });
544
- var HistorySessionSchema = z8.object({
545
- id: z8.string(),
546
- title: z8.string(),
547
- projectDir: z8.string(),
548
- updatedAt: z8.number(),
549
- provider: z8.enum(providerValues).optional(),
550
- preferredMode: z8.enum(sessionModeValues).optional()
587
+ var HistorySessionSchema = z9.object({
588
+ id: z9.string(),
589
+ title: z9.string(),
590
+ projectDir: z9.string(),
591
+ updatedAt: z9.number(),
592
+ provider: z9.enum(providerValues).optional(),
593
+ preferredMode: z9.enum(sessionModeValues).optional()
551
594
  });
552
- var SessionHistoryMessageSchema = z8.object({
553
- role: z8.enum(["user", "assistant", "system"]),
554
- text: z8.string(),
555
- timestamp: z8.number().optional(),
556
- cursor: z8.string().optional()
595
+ var SessionHistoryMessageSchema = z9.object({
596
+ role: z9.enum(["user", "assistant", "system"]),
597
+ text: z9.string(),
598
+ timestamp: z9.number().optional(),
599
+ cursor: z9.string().optional()
557
600
  });
558
601
  var RequestIdShape = { requestId: IdSchema.optional() };
559
602
  var RequiredRequestIdShape = { requestId: IdSchema };
560
- var ControlErrorCodeSchema = z8.enum(Object.values(ControlErrorCode));
603
+ var ControlErrorCodeSchema = z9.enum(Object.values(ControlErrorCode));
561
604
  var RequestErrorShape = {
562
- error: z8.string().optional(),
605
+ error: z9.string().optional(),
563
606
  errorCode: ControlErrorCodeSchema.optional()
564
607
  };
565
- var RemoteFileDispositionSchema = z8.enum(["inline", "download"]);
566
- var RemoteFileUploadKindSchema = z8.enum(["clipboard_image", "file"]);
608
+ var RemoteFileDispositionSchema = z9.enum(["inline", "download"]);
609
+ var RemoteFileUploadKindSchema = z9.enum(["clipboard_image", "file"]);
567
610
  function control(type, shape, directions) {
568
611
  return {
569
612
  type,
570
613
  directions: new Set(Array.isArray(directions) ? directions : directions ? [directions] : []),
571
- schema: z8.object({
572
- type: z8.literal(type),
614
+ schema: z9.object({
615
+ type: z9.literal(type),
573
616
  ...shape ?? {}
574
617
  })
575
618
  };
@@ -577,81 +620,81 @@ function control(type, shape, directions) {
577
620
  var relayControlDefinitions = [
578
621
  control("proxy_register", {
579
622
  proxyId: IdSchema,
580
- name: z8.string().optional()
623
+ name: z9.string().optional()
581
624
  }),
582
625
  control("proxy_register_response", {
583
- status: z8.enum(["new", "reconnected"])
626
+ status: z9.enum(["new", "reconnected"])
584
627
  }),
585
628
  control("proxy_list_request", RequestIdShape),
586
629
  control("proxy_list_response", {
587
630
  ...RequestIdShape,
588
- proxies: z8.array(ProxyInfoSchema)
631
+ proxies: z9.array(ProxyInfoSchema)
589
632
  }),
590
633
  control("relay_client_list_request", RequestIdShape),
591
634
  control("relay_client_list_response", {
592
635
  ...RequestIdShape,
593
- clients: z8.array(RelayClientInfoSchema)
636
+ clients: z9.array(RelayClientInfoSchema)
594
637
  }),
595
638
  control("relay_client_kick", { ...RequiredRequestIdShape, clientId: IdSchema }),
596
639
  control("relay_client_kick_response", {
597
640
  ...RequiredRequestIdShape,
598
641
  clientId: IdSchema,
599
- success: z8.boolean(),
642
+ success: z9.boolean(),
600
643
  ...RequestErrorShape
601
644
  }),
602
645
  control("relay_client_kicked", {
603
- reason: z8.string().optional()
646
+ reason: z9.string().optional()
604
647
  }),
605
648
  control("remote_file_url_request", {
606
649
  ...RequiredRequestIdShape,
607
650
  sessionId: IdSchema,
608
- path: z8.string().min(1),
651
+ path: z9.string().min(1),
609
652
  disposition: RemoteFileDispositionSchema
610
653
  }),
611
654
  control("remote_file_url_response", {
612
655
  ...RequiredRequestIdShape,
613
656
  ...RequestErrorShape,
614
657
  sessionId: IdSchema,
615
- path: z8.string().optional(),
616
- success: z8.boolean(),
617
- url: z8.string().optional(),
618
- expiresAt: z8.number().int().nonnegative().optional()
658
+ path: z9.string().optional(),
659
+ success: z9.boolean(),
660
+ url: z9.string().optional(),
661
+ expiresAt: z9.number().int().nonnegative().optional()
619
662
  }),
620
663
  control("remote_file_metadata_request", {
621
664
  ...RequiredRequestIdShape,
622
665
  sessionId: IdSchema,
623
- path: z8.string().min(1)
666
+ path: z9.string().min(1)
624
667
  }),
625
668
  control("remote_file_metadata_response", {
626
669
  ...RequiredRequestIdShape,
627
670
  ...RequestErrorShape,
628
671
  sessionId: IdSchema,
629
- path: z8.string().optional(),
630
- success: z8.boolean(),
631
- mimeType: z8.string().optional(),
632
- size: z8.number().int().nonnegative().optional(),
633
- fileName: z8.string().optional()
672
+ path: z9.string().optional(),
673
+ success: z9.boolean(),
674
+ mimeType: z9.string().optional(),
675
+ size: z9.number().int().nonnegative().optional(),
676
+ fileName: z9.string().optional()
634
677
  }),
635
678
  control("remote_file_stream_request", {
636
679
  streamId: IdSchema,
637
680
  sessionId: IdSchema,
638
- path: z8.string().min(1),
681
+ path: z9.string().min(1),
639
682
  disposition: RemoteFileDispositionSchema
640
683
  }),
641
684
  control("remote_file_stream_response", {
642
685
  ...RequestErrorShape,
643
686
  streamId: IdSchema,
644
687
  sessionId: IdSchema,
645
- success: z8.boolean(),
646
- path: z8.string().optional(),
647
- mimeType: z8.string().optional(),
648
- size: z8.number().int().nonnegative().optional(),
649
- fileName: z8.string().optional()
688
+ success: z9.boolean(),
689
+ path: z9.string().optional(),
690
+ mimeType: z9.string().optional(),
691
+ size: z9.number().int().nonnegative().optional(),
692
+ fileName: z9.string().optional()
650
693
  }),
651
694
  control("remote_file_stream_complete", {
652
695
  ...RequestErrorShape,
653
696
  streamId: IdSchema,
654
- success: z8.boolean()
697
+ success: z9.boolean()
655
698
  }),
656
699
  control("remote_file_stream_cancel", {
657
700
  streamId: IdSchema
@@ -660,25 +703,25 @@ var relayControlDefinitions = [
660
703
  ...RequiredRequestIdShape,
661
704
  sessionId: IdSchema,
662
705
  kind: RemoteFileUploadKindSchema,
663
- fileName: z8.string().optional(),
664
- mimeType: z8.string().min(1),
665
- size: z8.number().int().nonnegative().optional()
706
+ fileName: z9.string().optional(),
707
+ mimeType: z9.string().min(1),
708
+ size: z9.number().int().nonnegative().optional()
666
709
  }),
667
710
  control("remote_file_upload_url_response", {
668
711
  ...RequiredRequestIdShape,
669
712
  ...RequestErrorShape,
670
713
  sessionId: IdSchema,
671
- success: z8.boolean(),
672
- uploadUrl: z8.string().optional(),
673
- expiresAt: z8.number().int().nonnegative().optional()
714
+ success: z9.boolean(),
715
+ uploadUrl: z9.string().optional(),
716
+ expiresAt: z9.number().int().nonnegative().optional()
674
717
  }),
675
718
  control("remote_file_upload_stream_request", {
676
719
  uploadId: IdSchema,
677
720
  sessionId: IdSchema,
678
721
  kind: RemoteFileUploadKindSchema,
679
- fileName: z8.string().optional(),
680
- mimeType: z8.string().min(1),
681
- size: z8.number().int().nonnegative().optional()
722
+ fileName: z9.string().optional(),
723
+ mimeType: z9.string().min(1),
724
+ size: z9.number().int().nonnegative().optional()
682
725
  }),
683
726
  control("remote_file_upload_stream_complete", {
684
727
  uploadId: IdSchema
@@ -690,19 +733,19 @@ var relayControlDefinitions = [
690
733
  ...RequestErrorShape,
691
734
  uploadId: IdSchema,
692
735
  sessionId: IdSchema,
693
- success: z8.boolean(),
694
- path: z8.string().optional()
736
+ success: z9.boolean(),
737
+ path: z9.string().optional()
695
738
  }),
696
739
  control("proxy_select", { ...RequestIdShape, proxyId: IdSchema }),
697
740
  control("proxy_select_response", {
698
741
  ...RequestIdShape,
699
- success: z8.boolean(),
742
+ success: z9.boolean(),
700
743
  proxyId: IdSchema.optional(),
701
744
  ...RequestErrorShape
702
745
  }),
703
746
  control("relay_error", {
704
- code: z8.enum(Object.values(RelayErrorCode)),
705
- message: z8.string(),
747
+ code: z9.enum(Object.values(RelayErrorCode)),
748
+ message: z9.string(),
706
749
  // 可选 requestId: relay 把 client 发来 raw 的 requestId 字段透传回来,
707
750
  // client 侧 waitForMessage 据此把对应 pending request 立即拒掉而不必等到 timeout。
708
751
  requestId: IdSchema.optional()
@@ -721,7 +764,7 @@ var relayControlDefinitions = [
721
764
  control("voice_config_update_response", {
722
765
  ...RequestIdShape,
723
766
  ...RequestErrorShape,
724
- success: z8.boolean(),
767
+ success: z9.boolean(),
725
768
  config: VoiceProviderConfigSchema.optional()
726
769
  }),
727
770
  control("voice_config_test", {
@@ -731,15 +774,15 @@ var relayControlDefinitions = [
731
774
  control("voice_config_test_response", {
732
775
  ...RequestIdShape,
733
776
  ...RequestErrorShape,
734
- success: z8.boolean(),
735
- audioBase64: z8.string().optional(),
736
- audioSampleRate: z8.number().int().positive().optional(),
737
- audioEncoding: z8.literal("pcm_s16le").optional(),
738
- transcript: z8.string().optional()
777
+ success: z9.boolean(),
778
+ audioBase64: z9.string().optional(),
779
+ audioSampleRate: z9.number().int().positive().optional(),
780
+ audioEncoding: z9.literal("pcm_s16le").optional(),
781
+ transcript: z9.string().optional()
739
782
  }),
740
783
  control("voice_capabilities_request", {
741
784
  ...RequestIdShape,
742
- region: z8.enum(voiceRegionValues).optional()
785
+ region: z9.enum(voiceRegionValues).optional()
743
786
  }),
744
787
  control("voice_capabilities_response", {
745
788
  ...RequestIdShape,
@@ -751,37 +794,37 @@ var relayControlDefinitions = [
751
794
  control("latency_web_relay_ping", RequiredRequestIdShape),
752
795
  control("latency_web_relay_pong", {
753
796
  ...RequiredRequestIdShape,
754
- relayNow: z8.number().optional()
797
+ relayNow: z9.number().optional()
755
798
  }),
756
799
  control("latency_relay_proxy_request", RequiredRequestIdShape),
757
800
  control("latency_relay_proxy_response", {
758
801
  ...RequiredRequestIdShape,
759
- success: z8.boolean(),
760
- rttMs: z8.number().nonnegative().optional(),
761
- error: z8.string().optional()
802
+ success: z9.boolean(),
803
+ rttMs: z9.number().nonnegative().optional(),
804
+ error: z9.string().optional()
762
805
  }),
763
806
  control("latency_relay_proxy_ping", {
764
807
  ...RequiredRequestIdShape,
765
- relayNow: z8.number().optional()
808
+ relayNow: z9.number().optional()
766
809
  }),
767
810
  control("latency_relay_proxy_pong", {
768
811
  ...RequiredRequestIdShape,
769
- proxyNow: z8.number().optional()
812
+ proxyNow: z9.number().optional()
770
813
  }),
771
814
  control("latency_web_proxy_ping", RequiredRequestIdShape, "client_to_proxy"),
772
- control("latency_web_proxy_pong", { ...RequiredRequestIdShape, proxyNow: z8.number().optional() }, "proxy_to_client"),
815
+ control("latency_web_proxy_pong", { ...RequiredRequestIdShape, proxyNow: z9.number().optional() }, "proxy_to_client"),
773
816
  // 客户端注册协议
774
817
  control("client_register", {
775
818
  clientId: IdSchema,
776
- userAgent: z8.string().optional(),
777
- platform: z8.string().optional(),
778
- maxTouchPoints: z8.number().int().nonnegative().optional(),
779
- browserName: z8.string().min(1),
780
- osName: z8.string().min(1),
781
- deviceKind: z8.enum(["desktop", "tablet", "phone", "unknown"])
819
+ userAgent: z9.string().optional(),
820
+ platform: z9.string().optional(),
821
+ maxTouchPoints: z9.number().int().nonnegative().optional(),
822
+ browserName: z9.string().min(1),
823
+ osName: z9.string().min(1),
824
+ deviceKind: z9.enum(["desktop", "tablet", "phone", "unknown"])
782
825
  }),
783
826
  control("client_register_response", {
784
- status: z8.enum(["restored", "proxy_offline", "new"]),
827
+ status: z9.enum(["restored", "proxy_offline", "new"]),
785
828
  proxyId: IdSchema.optional()
786
829
  }),
787
830
  // Proxy 离线通知
@@ -800,51 +843,51 @@ var relayControlDefinitions = [
800
843
  control("dir_list_request", {
801
844
  proxyId: IdSchema.optional(),
802
845
  ...RequestIdShape,
803
- path: z8.string()
846
+ path: z9.string()
804
847
  }, "client_to_proxy"),
805
- control("dir_list_response", { ...RequestIdShape, ...RequestErrorShape, entries: z8.array(DirEntrySchema), path: z8.string() }, "proxy_to_client"),
848
+ control("dir_list_response", { ...RequestIdShape, ...RequestErrorShape, entries: z9.array(DirEntrySchema), path: z9.string() }, "proxy_to_client"),
806
849
  // 目录创建请求与响应
807
- control("dir_create_request", { ...RequestIdShape, path: z8.string() }, "client_to_proxy"),
850
+ control("dir_create_request", { ...RequestIdShape, path: z9.string() }, "client_to_proxy"),
808
851
  control("dir_create_response", {
809
852
  ...RequestIdShape,
810
853
  ...RequestErrorShape,
811
- path: z8.string(),
812
- success: z8.boolean()
854
+ path: z9.string(),
855
+ success: z9.boolean()
813
856
  }, "proxy_to_client"),
814
857
  // 命令列表推送,proxy 将可用命令列表推给 client
815
- control("command_list_push", { commands: z8.array(CommandEntrySchema) }, "proxy_to_client"),
858
+ control("command_list_push", { commands: z9.array(CommandEntrySchema) }, "proxy_to_client"),
816
859
  // 文件树推送: 按目录分组, 首组 path 即为 session cwd
817
860
  // 前端直接把每组写入 tree[path], 与 dir_list_response 共享 cache slot
818
861
  control("file_tree_push", {
819
- groups: z8.array(FileTreeGroupSchema)
862
+ groups: z9.array(FileTreeGroupSchema)
820
863
  }, "proxy_to_client"),
821
864
  // 会话列表请求与权限模式变更
822
865
  control("session_list", void 0, ["client_to_proxy", "proxy_to_client"]),
823
866
  control("permission_mode_change", {
824
- mode: z8.enum(["default", "auto_accept", "plan"]),
867
+ mode: z9.enum(["default", "auto_accept", "plan"]),
825
868
  // sessionId 可选:传入时 proxy 按该会话的 mode 分叉(PTY 发 Tab ANSI),未传走全局日志行为
826
869
  sessionId: IdSchema.optional()
827
870
  }, "client_to_proxy"),
828
871
  // 会话历史浏览
829
872
  control("session_history_request", RequestIdShape, "client_to_proxy"),
830
- control("session_history_response", { ...RequestIdShape, sessions: z8.array(HistorySessionSchema) }, "proxy_to_client"),
873
+ control("session_history_response", { ...RequestIdShape, sessions: z9.array(HistorySessionSchema) }, "proxy_to_client"),
831
874
  // PTY 语义状态,从 Envelope 迁移到 Control 层
832
875
  control("pty_state", { sessionId: IdSchema, payload: PtyStatePayloadSchema }, "proxy_to_client"),
833
876
  // Provider 语义状态,来自 Claude/Codex hook 等结构化事件,不从 PTY 字节推断
834
877
  control("agent_status", { sessionId: IdSchema, payload: AgentStatusPayloadSchema }, "proxy_to_client"),
835
878
  // 终端标题变化,proxy -> client
836
- control("terminal_title", { sessionId: IdSchema, title: z8.string() }, "proxy_to_client"),
879
+ control("terminal_title", { sessionId: IdSchema, title: z9.string() }, "proxy_to_client"),
837
880
  // 终端尺寸变化,proxy -> client
838
- control("terminal_resize", { sessionId: IdSchema, cols: z8.number().int().positive(), rows: z8.number().int().positive() }, "proxy_to_client"),
839
- control("terminal_resize_request", { sessionId: IdSchema, cols: z8.number().int().positive(), rows: z8.number().int().positive() }, "client_to_proxy"),
881
+ control("terminal_resize", { sessionId: IdSchema, cols: z9.number().int().positive(), rows: z9.number().int().positive() }, "proxy_to_client"),
882
+ control("terminal_resize_request", { sessionId: IdSchema, cols: z9.number().int().positive(), rows: z9.number().int().positive() }, "client_to_proxy"),
840
883
  // 远程终止 JSON 会话,client -> proxy
841
884
  control("session_terminate", { sessionId: IdSchema }, "client_to_proxy"),
842
- control("session_rename", { ...RequestIdShape, sessionId: IdSchema, name: z8.string() }, "client_to_proxy"),
885
+ control("session_rename", { ...RequestIdShape, sessionId: IdSchema, name: z9.string() }, "client_to_proxy"),
843
886
  control("session_rename_response", {
844
887
  ...RequestIdShape,
845
888
  sessionId: IdSchema,
846
- success: z8.boolean(),
847
- name: z8.string().optional(),
889
+ success: z9.boolean(),
890
+ name: z9.string().optional(),
848
891
  ...RequestErrorShape
849
892
  }, "proxy_to_client"),
850
893
  // 中断当前 turn,client -> proxy;JSON 由 session-worker 打断 Claude 子进程并保留会话。
@@ -852,55 +895,55 @@ var relayControlDefinitions = [
852
895
  // turn 完成信号,proxy -> client,对应 claude stream-json 的 result 事件
853
896
  control("turn_result", {
854
897
  sessionId: IdSchema,
855
- success: z8.boolean(),
856
- isError: z8.boolean(),
898
+ success: z9.boolean(),
899
+ isError: z9.boolean(),
857
900
  // stream-json result.result 是本轮最终文本。assistant_message 流丢失或 CLI 未发增量时,
858
901
  // Web 用它作为 JSON 模式兜底展示,避免 turn 已结束但界面空白。
859
- result: z8.string().optional()
902
+ result: z9.string().optional()
860
903
  }, "proxy_to_client"),
861
904
  // 客户端发送到 PTY 的原始字节(ANSI 序列),不追加换行
862
- control("remote_input_raw", { sessionId: IdSchema, data: z8.string(), traceId: IdSchema.optional() }, "client_to_proxy"),
905
+ control("remote_input_raw", { sessionId: IdSchema, data: z9.string(), traceId: IdSchema.optional() }, "client_to_proxy"),
863
906
  // 客户端询问 proxy 的环境信息 (home 路径等), client -> proxy -> response
864
907
  // FilePathPicker 用 homePath 作为 select 模式下的默认起点, 新建会话时打开即可浏览
865
908
  control("proxy_info_request", RequestIdShape, "client_to_proxy"),
866
- control("proxy_info", { ...RequestIdShape, homePath: z8.string(), agentCli: AgentCliStatusSchema }, "proxy_to_client"),
867
- control("agent_cli_config_update", { ...RequestIdShape, provider: z8.enum(providerValues), path: z8.string().min(1) }, "client_to_proxy"),
909
+ control("proxy_info", { ...RequestIdShape, homePath: z9.string(), agentCli: AgentCliStatusSchema }, "proxy_to_client"),
910
+ control("agent_cli_config_update", { ...RequestIdShape, provider: z9.enum(providerValues), path: z9.string().min(1) }, "client_to_proxy"),
868
911
  control("agent_cli_config_update_response", {
869
912
  ...RequestIdShape,
870
- provider: z8.enum(providerValues),
913
+ provider: z9.enum(providerValues),
871
914
  agentCli: AgentCliStatusSchema.optional(),
872
915
  ...RequestErrorShape
873
916
  }, "proxy_to_client"),
874
917
  // 远程创建 JSON 会话,client -> proxy -> response
875
918
  control("session_create", {
876
919
  ...RequestIdShape,
877
- kind: z8.enum(sessionKindValues).optional(),
878
- cwd: z8.string().optional(),
879
- name: z8.string().optional(),
880
- provider: z8.enum(providerValues).optional(),
881
- mode: z8.enum(sessionModeValues).optional(),
882
- resumeSessionId: z8.string().optional(),
920
+ kind: z9.enum(sessionKindValues).optional(),
921
+ cwd: z9.string().optional(),
922
+ name: z9.string().optional(),
923
+ provider: z9.enum(providerValues).optional(),
924
+ mode: z9.enum(sessionModeValues).optional(),
925
+ resumeSessionId: z9.string().optional(),
883
926
  // 透传给 claude CLI 的 --permission-mode, undefined 时 proxy 兜底为 "default"
884
- permissionMode: z8.enum(["default", "auto", "acceptEdits", "plan", "bypassPermissions", "dontAsk"]).optional()
927
+ permissionMode: z9.enum(["default", "auto", "acceptEdits", "plan", "bypassPermissions", "dontAsk"]).optional()
885
928
  }, "client_to_proxy"),
886
929
  control("session_create_response", {
887
930
  ...RequestIdShape,
888
931
  // 失败路径只送 errorCode/error, sessionId 此时无语义。成功路径才有 id。
889
932
  sessionId: IdSchema.optional(),
890
- name: z8.string().optional(),
891
- nameLocked: z8.boolean().optional(),
892
- kind: z8.enum(sessionKindValues).optional(),
893
- mode: z8.enum(sessionModeValues).optional(),
894
- provider: z8.enum(providerValues).optional(),
895
- ptyOwner: z8.enum(ptyOwnerValues).optional(),
933
+ name: z9.string().optional(),
934
+ nameLocked: z9.boolean().optional(),
935
+ kind: z9.enum(sessionKindValues).optional(),
936
+ mode: z9.enum(sessionModeValues).optional(),
937
+ provider: z9.enum(providerValues).optional(),
938
+ ptyOwner: z9.enum(ptyOwnerValues).optional(),
896
939
  ...RequestErrorShape
897
940
  }, "proxy_to_client"),
898
941
  // 客户端请求会话历史消息,client -> proxy
899
942
  control("session_messages_request", {
900
943
  ...RequestIdShape,
901
944
  sessionId: IdSchema,
902
- limit: z8.number().int().min(1).max(200).optional(),
903
- before: z8.string().optional()
945
+ limit: z9.number().int().min(1).max(200).optional(),
946
+ before: z9.string().optional()
904
947
  }, "client_to_proxy"),
905
948
  // 客户端请求会话资源(命令列表 + 文件树),client -> proxy
906
949
  control("session_resources_request", { ...RequestIdShape, sessionId: IdSchema }, "client_to_proxy"),
@@ -908,14 +951,14 @@ var relayControlDefinitions = [
908
951
  ...RequestIdShape,
909
952
  ...RequestErrorShape,
910
953
  sessionId: IdSchema,
911
- commands: z8.array(CommandEntrySchema),
912
- groups: z8.array(FileTreeGroupSchema)
954
+ commands: z9.array(CommandEntrySchema),
955
+ groups: z9.array(FileTreeGroupSchema)
913
956
  }, "proxy_to_client"),
914
957
  // 客户端请求当前 provider 语义状态;不经 relay 缓存,由 proxy 返回当前值
915
958
  control("agent_status_request", { ...RequestIdShape, sessionId: IdSchema.optional() }, "client_to_proxy"),
916
959
  control("agent_status_response", {
917
960
  ...RequestIdShape,
918
- statuses: z8.array(z8.object({ sessionId: IdSchema, payload: AgentStatusPayloadSchema }))
961
+ statuses: z9.array(z9.object({ sessionId: IdSchema, payload: AgentStatusPayloadSchema }))
919
962
  }, "proxy_to_client"),
920
963
  // 客户端确认已收到审批请求;proxy 只记录送达状态,不把它当成用户决策
921
964
  control("permission_request_delivered", { sessionId: IdSchema, requestId: IdSchema }, "client_to_proxy"),
@@ -925,17 +968,17 @@ var relayControlDefinitions = [
925
968
  control("permission_decision_result", {
926
969
  sessionId: IdSchema,
927
970
  requestId: IdSchema,
928
- outcome: z8.enum(["allow", "deny"]),
929
- delivered: z8.boolean(),
930
- message: z8.string().optional()
971
+ outcome: z9.enum(["allow", "deny"]),
972
+ delivered: z9.boolean(),
973
+ message: z9.string().optional()
931
974
  }, "proxy_to_client"),
932
975
  // proxy 推送当前 pending 的工具审批列表,client 据此恢复审批卡片
933
976
  control("pending_approvals_push", {
934
977
  sessionId: IdSchema,
935
- approvals: z8.array(z8.object({
978
+ approvals: z9.array(z9.object({
936
979
  requestId: IdSchema,
937
- toolName: z8.string(),
938
- input: z8.record(z8.string(), z8.unknown())
980
+ toolName: z9.string(),
981
+ input: z9.record(z9.string(), z9.unknown())
939
982
  }))
940
983
  }, "proxy_to_client"),
941
984
  // Voice Pilot speech summaries are produced by proxy-side Claude Code so it can read project context.
@@ -943,7 +986,7 @@ var relayControlDefinitions = [
943
986
  ...RequestIdShape,
944
987
  sessionId: IdSchema,
945
988
  messageId: IdSchema,
946
- text: z8.string().min(1),
989
+ text: z9.string().min(1),
947
990
  reason: VoiceSummaryReasonSchema
948
991
  }, "client_to_proxy"),
949
992
  control("voice_summary_response", {
@@ -951,31 +994,31 @@ var relayControlDefinitions = [
951
994
  ...RequestErrorShape,
952
995
  sessionId: IdSchema,
953
996
  messageId: IdSchema,
954
- success: z8.boolean(),
955
- summary: z8.string().min(1).optional()
997
+ success: z9.boolean(),
998
+ summary: z9.string().min(1).optional()
956
999
  }, "proxy_to_client"),
957
1000
  // 恢复会话时推送历史消息,proxy -> client
958
1001
  control("session_history_messages", {
959
1002
  ...RequestIdShape,
960
1003
  sessionId: IdSchema,
961
- before: z8.string().optional(),
962
- messages: z8.array(SessionHistoryMessageSchema),
963
- hasMore: z8.boolean().optional(),
964
- nextBefore: z8.string().optional()
1004
+ before: z9.string().optional(),
1005
+ messages: z9.array(SessionHistoryMessageSchema),
1006
+ hasMore: z9.boolean().optional(),
1007
+ nextBefore: z9.string().optional()
965
1008
  }, "proxy_to_client"),
966
1009
  // proxy 重连后同步活跃 session 列表给 relay。session_sync 由 relay 自消费(更新 proxy-session
967
1010
  // 关联)不转发给 client,因此**没有** direction 标注——RelayControlDirection 只描述转发流。
968
1011
  control("session_sync", {
969
- sessions: z8.array(z8.object({
970
- id: z8.string(),
971
- kind: z8.enum(sessionKindValues).optional(),
972
- mode: z8.enum(sessionModeValues),
973
- provider: z8.enum(providerValues),
974
- ptyOwner: z8.enum(ptyOwnerValues).optional(),
975
- cwd: z8.string().optional(),
976
- name: z8.string().optional(),
977
- nameLocked: z8.boolean().optional(),
978
- state: z8.enum(sessionStateValues)
1012
+ sessions: z9.array(z9.object({
1013
+ id: z9.string(),
1014
+ kind: z9.enum(sessionKindValues).optional(),
1015
+ mode: z9.enum(sessionModeValues),
1016
+ provider: z9.enum(providerValues),
1017
+ ptyOwner: z9.enum(ptyOwnerValues).optional(),
1018
+ cwd: z9.string().optional(),
1019
+ name: z9.string().optional(),
1020
+ nameLocked: z9.boolean().optional(),
1021
+ state: z9.enum(sessionStateValues)
979
1022
  }))
980
1023
  }),
981
1024
  // PTY 会话订阅,client -> proxy,触发 terminal serialize() 返回当前状态
@@ -983,15 +1026,15 @@ var relayControlDefinitions = [
983
1026
  // PTY 会话快照,proxy -> client,serialize() 的全量终端状态
984
1027
  control("session_snapshot", {
985
1028
  sessionId: IdSchema,
986
- cols: z8.number().int().positive(),
987
- rows: z8.number().int().positive(),
988
- data: z8.string(),
989
- outputSeq: z8.number().int().nonnegative(),
1029
+ cols: z9.number().int().positive(),
1030
+ rows: z9.number().int().positive(),
1031
+ data: z9.string(),
1032
+ outputSeq: z9.number().int().nonnegative(),
990
1033
  requestId: IdSchema.optional()
991
1034
  }, "proxy_to_client")
992
1035
  ];
993
1036
  var relayControlSchemas = relayControlDefinitions.map((definition) => definition.schema);
994
- var RelayControlSchema = z8.discriminatedUnion("type", relayControlSchemas);
1037
+ var RelayControlSchema = z9.discriminatedUnion("type", relayControlSchemas);
995
1038
  var ProxyToClientRelayControlTypes = new Set(relayControlDefinitions.filter((definition) => definition.directions.has("proxy_to_client")).map((definition) => definition.type));
996
1039
  function isProxyToClientRelayControlType(type) {
997
1040
  return ProxyToClientRelayControlTypes.has(type);
@@ -1001,6 +1044,11 @@ function isClientToProxyRelayControlType(type) {
1001
1044
  return ClientToProxyRelayControlTypes.has(type);
1002
1045
  }
1003
1046
 
1047
+ // ../../packages/shared/dist/constants/session-create.js
1048
+ var SESSION_CREATE_SERVER_DEADLINE_MS = 3e4;
1049
+ var SESSION_CREATE_RESPONSE_GRACE_MS = 5e3;
1050
+ var SESSION_CREATE_CLIENT_TIMEOUT_MS = SESSION_CREATE_SERVER_DEADLINE_MS + SESSION_CREATE_RESPONSE_GRACE_MS;
1051
+
1004
1052
  // ../../packages/shared/dist/constants/relay-close-codes.js
1005
1053
  var RelayCloseCode = {
1006
1054
  CLIENT_KICKED: 4401,
@@ -1052,6 +1100,25 @@ function decodeFileStreamFrame(view) {
1052
1100
  return { streamId, chunkSeq, data };
1053
1101
  }
1054
1102
 
1103
+ // ../../packages/shared/dist/voice-audio-codec.js
1104
+ var MU_LAW_BIAS = 132;
1105
+ function decodeSample(encodedValue) {
1106
+ const value = ~encodedValue & 255;
1107
+ const sign = value & 128;
1108
+ const exponent = value >> 4 & 7;
1109
+ const mantissa = value & 15;
1110
+ const magnitude = ((mantissa << 3) + MU_LAW_BIAS) * 2 ** exponent - MU_LAW_BIAS;
1111
+ return sign ? -magnitude : magnitude;
1112
+ }
1113
+ function decodeMuLawToPcm16(encoded) {
1114
+ const pcm = new Uint8Array(encoded.byteLength * 2);
1115
+ const view = new DataView(pcm.buffer);
1116
+ for (let index = 0; index < encoded.byteLength; index += 1) {
1117
+ view.setInt16(index * 2, decodeSample(encoded[index] ?? 255), true);
1118
+ }
1119
+ return pcm;
1120
+ }
1121
+
1055
1122
  // ../../packages/shared/dist/state-machine.js
1056
1123
  function computeAbsorbingSet(transitions) {
1057
1124
  const absorbing = /* @__PURE__ */ new Set();
@@ -1228,6 +1295,14 @@ var RelayRegistry = class {
1228
1295
  binding.ws = null;
1229
1296
  }
1230
1297
  }
1298
+ // 连接关闭只能释放自己持有的绑定。旧连接的 close 可能晚于新连接注册,
1299
+ // 此时不能把已经切换到新 WebSocket 的绑定清空。
1300
+ unbindClientSocket(clientId, ws) {
1301
+ const binding = this.clientBindings.get(clientId);
1302
+ if (!binding || binding.ws !== ws) return false;
1303
+ binding.ws = null;
1304
+ return true;
1305
+ }
1231
1306
  getClientBinding(clientId) {
1232
1307
  return this.clientBindings.get(clientId);
1233
1308
  }
@@ -1899,6 +1974,7 @@ function bailianInferenceUrl(region) {
1899
1974
  // src/voice/bailian-asr.ts
1900
1975
  var OPEN = 1;
1901
1976
  var END_OF_SPEECH_SILENCE_MS = 1200;
1977
+ var NOISE_RESISTANT_SERVER_VAD_THRESHOLD = 0.8;
1902
1978
  function defaultSocketFactory(url, options) {
1903
1979
  return new WebSocket5(url, options);
1904
1980
  }
@@ -1974,6 +2050,7 @@ var BailianAsrClientImpl = class extends EventEmitter {
1974
2050
  socket;
1975
2051
  isOpen = false;
1976
2052
  isReady = false;
2053
+ hasFinalTranscript = false;
1977
2054
  pending = [];
1978
2055
  sendPcm(chunk) {
1979
2056
  this.sendWhenReady({
@@ -2005,7 +2082,7 @@ var BailianAsrClientImpl = class extends EventEmitter {
2005
2082
  },
2006
2083
  turn_detection: {
2007
2084
  type: "server_vad",
2008
- threshold: 0,
2085
+ threshold: NOISE_RESISTANT_SERVER_VAD_THRESHOLD,
2009
2086
  silence_duration_ms: END_OF_SPEECH_SILENCE_MS
2010
2087
  }
2011
2088
  }
@@ -2041,11 +2118,21 @@ var BailianAsrClientImpl = class extends EventEmitter {
2041
2118
  if (preview) this.emit("partial", preview);
2042
2119
  return;
2043
2120
  }
2044
- if (type === "conversation.item.input_audio_transcription.completed" || type === "session.finished") {
2121
+ if (type === "conversation.item.input_audio_transcription.completed") {
2045
2122
  const transcript = extractFinalText(payload);
2046
2123
  if (transcript) {
2124
+ this.hasFinalTranscript = true;
2125
+ this.emit("final", transcript);
2126
+ }
2127
+ return;
2128
+ }
2129
+ if (type === "session.finished") {
2130
+ const transcript = extractFinalText(payload);
2131
+ if (transcript && !this.hasFinalTranscript) {
2132
+ this.hasFinalTranscript = true;
2047
2133
  this.emit("final", transcript);
2048
2134
  }
2135
+ this.socket.close(1e3, "session-finished");
2049
2136
  }
2050
2137
  }
2051
2138
  sendWhenReady(payload) {
@@ -2230,8 +2317,48 @@ function createBailianTtsClient(config, options = {}) {
2230
2317
  );
2231
2318
  }
2232
2319
 
2320
+ // src/voice/pcm-resample.ts
2321
+ import waveResampler from "wave-resampler";
2322
+ var RESAMPLE_DETAILS = {
2323
+ method: "sinc",
2324
+ LPF: true,
2325
+ LPFType: "FIR"
2326
+ };
2327
+ function resampleMonoPcmS16le(audio, inputSampleRate, outputSampleRate) {
2328
+ if (audio.byteLength % 2 !== 0) {
2329
+ throw new Error("PCM \u97F3\u9891\u6570\u636E\u957F\u5EA6\u65E0\u6548");
2330
+ }
2331
+ if (!validSampleRate(inputSampleRate) || !validSampleRate(outputSampleRate)) {
2332
+ throw new Error("PCM \u97F3\u9891\u91C7\u6837\u7387\u65E0\u6548");
2333
+ }
2334
+ if (inputSampleRate === outputSampleRate) return Buffer.from(audio);
2335
+ const input = new Int16Array(audio.byteLength / 2);
2336
+ for (let index = 0; index < input.length; index += 1) {
2337
+ input[index] = audio.readInt16LE(index * 2);
2338
+ }
2339
+ const samples = waveResampler.resample(
2340
+ input,
2341
+ inputSampleRate,
2342
+ outputSampleRate,
2343
+ RESAMPLE_DETAILS
2344
+ );
2345
+ const output = Buffer.alloc(samples.length * 2);
2346
+ for (let index = 0; index < samples.length; index += 1) {
2347
+ const sample = samples[index];
2348
+ if (typeof sample !== "number" || !Number.isFinite(sample)) {
2349
+ throw new Error("PCM \u97F3\u9891\u91CD\u91C7\u6837\u5931\u8D25");
2350
+ }
2351
+ output.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(sample))), index * 2);
2352
+ }
2353
+ return output;
2354
+ }
2355
+ function validSampleRate(sampleRate) {
2356
+ return Number.isSafeInteger(sampleRate) && sampleRate > 0;
2357
+ }
2358
+
2233
2359
  // src/voice/config-test.ts
2234
- var TEST_SAMPLE_RATE = 16e3;
2360
+ var TTS_TEST_SAMPLE_RATE = 24e3;
2361
+ var ASR_TEST_SAMPLE_RATE = 16e3;
2235
2362
  var ASR_TEST_CHUNK_BYTES = 3200;
2236
2363
  var ASR_TEST_CHUNK_INTERVAL_MS = 100;
2237
2364
  function mergeVoiceConfigForTest(current, update) {
@@ -2263,14 +2390,19 @@ function createBailianVoiceConfigTester(options = {}) {
2263
2390
  timeoutMs,
2264
2391
  clientFactory: ttsClientFactory
2265
2392
  });
2393
+ const recognitionAudio = resampleMonoPcmS16le(
2394
+ audio,
2395
+ TTS_TEST_SAMPLE_RATE,
2396
+ ASR_TEST_SAMPLE_RATE
2397
+ );
2266
2398
  const transcript = await recognizeTestAudio({
2267
2399
  config,
2268
- audio,
2400
+ audio: recognitionAudio,
2269
2401
  sampleText,
2270
2402
  timeoutMs,
2271
2403
  clientFactory: asrClientFactory
2272
2404
  });
2273
- return { audio, sampleRate: TEST_SAMPLE_RATE, transcript };
2405
+ return { audio, sampleRate: TTS_TEST_SAMPLE_RATE, transcript };
2274
2406
  }
2275
2407
  };
2276
2408
  }
@@ -2281,7 +2413,7 @@ function synthesizeTestAudio(options) {
2281
2413
  region: config.region,
2282
2414
  model: config.ttsModel,
2283
2415
  voice: config.ttsVoice,
2284
- sampleRate: TEST_SAMPLE_RATE
2416
+ sampleRate: TTS_TEST_SAMPLE_RATE
2285
2417
  });
2286
2418
  return new Promise((resolve2, reject) => {
2287
2419
  let settled = false;
@@ -2324,7 +2456,7 @@ function recognizeTestAudio(options) {
2324
2456
  apiKey: config.apiKey,
2325
2457
  region: config.region,
2326
2458
  model: config.asrModel,
2327
- sampleRate: TEST_SAMPLE_RATE,
2459
+ sampleRate: ASR_TEST_SAMPLE_RATE,
2328
2460
  language: "zh"
2329
2461
  });
2330
2462
  return new Promise((resolve2, reject) => {
@@ -3036,7 +3168,7 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
3036
3168
  clientWs.on("close", (code, reason) => {
3037
3169
  registry.removeClientWs(clientWs);
3038
3170
  if (clientWs.clientId) {
3039
- registry.unbindClientById(clientWs.clientId);
3171
+ registry.unbindClientSocket(clientWs.clientId, clientWs);
3040
3172
  }
3041
3173
  logger.info(
3042
3174
  {
@@ -3242,59 +3374,190 @@ function parseJson(data) {
3242
3374
  return null;
3243
3375
  }
3244
3376
  }
3245
- function isStartMessage(payload) {
3246
- if (!payload || typeof payload !== "object") return false;
3247
- const record = payload;
3248
- return record.type === "start" && typeof record.sessionId === "string";
3377
+ function buildStats(payload, config) {
3378
+ return {
3379
+ sessionId: payload.sessionId,
3380
+ attemptId: payload.attemptId,
3381
+ sampleRate: payload.sampleRate,
3382
+ provider: config.provider,
3383
+ region: config.region,
3384
+ asrModel: config.asrModel,
3385
+ encoding: payload.encoding,
3386
+ startedAt: Date.now(),
3387
+ readyAt: null,
3388
+ firstPcmAt: null,
3389
+ encodedBytes: 0,
3390
+ pcmBytes: 0,
3391
+ pcmChunks: 0,
3392
+ partialEvents: 0,
3393
+ finalEvents: 0,
3394
+ finalChars: 0
3395
+ };
3396
+ }
3397
+ function statsLogFields(stats) {
3398
+ const now = Date.now();
3399
+ return {
3400
+ sessionId: stats.sessionId,
3401
+ attemptId: stats.attemptId,
3402
+ sampleRate: stats.sampleRate,
3403
+ provider: stats.provider,
3404
+ region: stats.region,
3405
+ asrModel: stats.asrModel,
3406
+ encoding: stats.encoding,
3407
+ encodedBytes: stats.encodedBytes,
3408
+ pcmBytes: stats.pcmBytes,
3409
+ pcmChunks: stats.pcmChunks,
3410
+ partialEvents: stats.partialEvents,
3411
+ finalEvents: stats.finalEvents,
3412
+ finalChars: stats.finalChars,
3413
+ durationMs: now - stats.startedAt,
3414
+ readyMs: stats.readyAt === null ? null : stats.readyAt - stats.startedAt,
3415
+ firstPcmMs: stats.firstPcmAt === null ? null : stats.firstPcmAt - stats.startedAt
3416
+ };
3249
3417
  }
3250
3418
  function handleVoiceAsrConnection(ws, store, logger, providers) {
3251
3419
  let provider = null;
3420
+ let activeStats = null;
3421
+ function isCurrent(candidate, stats, event) {
3422
+ if (provider === candidate && activeStats === stats) return true;
3423
+ logger.debug(
3424
+ { attemptId: stats.attemptId, activeAttemptId: activeStats?.attemptId ?? null, event },
3425
+ "Ignored stale Voice ASR provider event"
3426
+ );
3427
+ return false;
3428
+ }
3429
+ function detachProvider(reason) {
3430
+ const previousProvider = provider;
3431
+ const previousStats = activeStats;
3432
+ provider = null;
3433
+ activeStats = null;
3434
+ if (previousStats) {
3435
+ logger.info({ ...statsLogFields(previousStats), reason }, "Voice ASR attempt detached");
3436
+ }
3437
+ previousProvider?.close();
3438
+ }
3252
3439
  function start(payload) {
3253
3440
  const config = store.readSecret();
3254
3441
  if (!config.apiKey) {
3255
3442
  sendJson(ws, {
3256
3443
  type: "error",
3444
+ attemptId: payload.attemptId,
3257
3445
  errorCode: "not_configured",
3258
3446
  error: "Voice provider is not configured"
3259
3447
  });
3260
3448
  return;
3261
3449
  }
3262
- provider?.close();
3263
- provider = providers.current(config).createAsrClient(config, {
3264
- sampleRate: payload.sampleRate ?? 16e3,
3265
- language: "zh"
3450
+ detachProvider("replaced");
3451
+ const stats = buildStats(payload, config);
3452
+ let currentProvider;
3453
+ try {
3454
+ currentProvider = providers.current(config).createAsrClient(config, {
3455
+ sampleRate: stats.sampleRate,
3456
+ language: "zh"
3457
+ });
3458
+ } catch (error) {
3459
+ logger.warn({ ...statsLogFields(stats), err: error }, "Voice ASR provider creation failed");
3460
+ sendJson(ws, {
3461
+ type: "error",
3462
+ attemptId: payload.attemptId,
3463
+ error: error instanceof Error ? error.message : "ASR failed"
3464
+ });
3465
+ return;
3466
+ }
3467
+ provider = currentProvider;
3468
+ activeStats = stats;
3469
+ logger.info(statsLogFields(stats), "Voice ASR attempt started");
3470
+ currentProvider.on("ready", () => {
3471
+ if (!isCurrent(currentProvider, stats, "ready")) return;
3472
+ stats.readyAt = Date.now();
3473
+ logger.info(statsLogFields(stats), "Voice ASR provider ready");
3474
+ sendJson(ws, { type: "ready", attemptId: stats.attemptId });
3475
+ });
3476
+ currentProvider.on("partial", (text) => {
3477
+ if (!isCurrent(currentProvider, stats, "partial")) return;
3478
+ stats.partialEvents += 1;
3479
+ sendJson(ws, { type: "partial", attemptId: stats.attemptId, text });
3480
+ });
3481
+ currentProvider.on("final", (text) => {
3482
+ if (!isCurrent(currentProvider, stats, "final")) return;
3483
+ stats.finalEvents += 1;
3484
+ stats.finalChars += text.length;
3485
+ sendJson(ws, { type: "final", attemptId: stats.attemptId, text });
3486
+ });
3487
+ currentProvider.on("error", (error) => {
3488
+ if (!isCurrent(currentProvider, stats, "error")) return;
3489
+ logger.warn({ ...statsLogFields(stats), err: error }, "Voice ASR provider reported an error");
3490
+ sendJson(ws, {
3491
+ type: "error",
3492
+ attemptId: stats.attemptId,
3493
+ error: error.message || "ASR failed"
3494
+ });
3495
+ });
3496
+ currentProvider.on("closed", (code, reason) => {
3497
+ if (!isCurrent(currentProvider, stats, "closed")) return;
3498
+ provider = null;
3499
+ activeStats = null;
3500
+ logger.info({ ...statsLogFields(stats), code, reason }, "Voice ASR provider closed");
3501
+ sendJson(ws, { type: "closed", attemptId: stats.attemptId, code, reason });
3266
3502
  });
3267
- provider.on("ready", () => sendJson(ws, { type: "ready" }));
3268
- provider.on("partial", (text) => sendJson(ws, { type: "partial", text }));
3269
- provider.on("final", (text) => sendJson(ws, { type: "final", text }));
3270
- provider.on(
3271
- "error",
3272
- (error) => sendJson(ws, { type: "error", error: error.message || "ASR failed" })
3273
- );
3274
- provider.on("closed", (code, reason) => sendJson(ws, { type: "closed", code, reason }));
3275
3503
  }
3276
3504
  ws.on("message", (data, isBinary) => {
3277
3505
  if (isBinary) {
3278
- provider?.sendPcm(toBuffer(data));
3506
+ const encodedChunk = toBuffer(data);
3507
+ if (provider && activeStats) {
3508
+ const pcmChunk = activeStats.encoding === "mulaw" ? Buffer.from(decodeMuLawToPcm16(encodedChunk)) : encodedChunk;
3509
+ if (activeStats.firstPcmAt === null) activeStats.firstPcmAt = Date.now();
3510
+ activeStats.encodedBytes += encodedChunk.byteLength;
3511
+ activeStats.pcmBytes += pcmChunk.byteLength;
3512
+ activeStats.pcmChunks += 1;
3513
+ provider.sendPcm(pcmChunk);
3514
+ sendJson(ws, {
3515
+ type: "audio_ack",
3516
+ attemptId: activeStats.attemptId,
3517
+ encodedBytes: activeStats.encodedBytes,
3518
+ pcmBytes: activeStats.pcmBytes,
3519
+ chunks: activeStats.pcmChunks
3520
+ });
3521
+ }
3522
+ return;
3523
+ }
3524
+ const parsed = VoiceAsrClientMessageSchema.safeParse(parseJson(data));
3525
+ if (!parsed.success) {
3526
+ logger.debug({ issues: parsed.error.issues }, "Ignored invalid Voice ASR client message");
3279
3527
  return;
3280
3528
  }
3281
- const payload = parseJson(data);
3282
- if (isStartMessage(payload)) {
3529
+ const payload = parsed.data;
3530
+ if (payload.type === "start") {
3283
3531
  start(payload);
3284
3532
  return;
3285
3533
  }
3286
- if (payload && typeof payload === "object" && payload.type === "stop") {
3287
- provider?.stop();
3534
+ if (payload.type === "stop") {
3535
+ if (!provider || !activeStats || activeStats.attemptId !== payload.attemptId) {
3536
+ logger.debug(
3537
+ { attemptId: payload.attemptId, activeAttemptId: activeStats?.attemptId ?? null },
3538
+ "Ignored stale Voice ASR stop"
3539
+ );
3540
+ return;
3541
+ }
3542
+ logger.info(statsLogFields(activeStats), "Voice ASR stop requested");
3543
+ provider.stop();
3288
3544
  }
3289
3545
  });
3290
- ws.on("close", () => {
3291
- provider?.close();
3292
- provider = null;
3546
+ ws.on("close", (code, reason) => {
3547
+ if (activeStats) {
3548
+ logger.info(
3549
+ { ...statsLogFields(activeStats), code, reason: reason.toString("utf8") },
3550
+ "Voice ASR client websocket closed"
3551
+ );
3552
+ }
3553
+ detachProvider("client-websocket-closed");
3293
3554
  });
3294
3555
  ws.on("error", (err) => {
3295
- logger.warn({ err }, "Voice ASR websocket error");
3296
- provider?.close();
3297
- provider = null;
3556
+ logger.warn(
3557
+ { ...activeStats ? statsLogFields(activeStats) : {}, err },
3558
+ "Voice ASR websocket error"
3559
+ );
3560
+ detachProvider("client-websocket-error");
3298
3561
  });
3299
3562
  }
3300
3563
 
@@ -3321,7 +3584,7 @@ function isSpeakMessage(payload) {
3321
3584
  function nowMs() {
3322
3585
  return Date.now();
3323
3586
  }
3324
- function buildStats(requestId, text, config) {
3587
+ function buildStats2(requestId, text, config) {
3325
3588
  return {
3326
3589
  requestId,
3327
3590
  textChars: text.length,
@@ -3335,7 +3598,7 @@ function buildStats(requestId, text, config) {
3335
3598
  audioChunks: 0
3336
3599
  };
3337
3600
  }
3338
- function statsLogFields(stats) {
3601
+ function statsLogFields2(stats) {
3339
3602
  const currentTime = nowMs();
3340
3603
  return {
3341
3604
  requestId: stats.requestId,
@@ -3375,7 +3638,7 @@ function handleVoiceTtsConnection(ws, store, logger, providers) {
3375
3638
  });
3376
3639
  providerConfig = config;
3377
3640
  provider.on("started", () => {
3378
- if (activeStats) logger.info(statsLogFields(activeStats), "Voice TTS started");
3641
+ if (activeStats) logger.info(statsLogFields2(activeStats), "Voice TTS started");
3379
3642
  sendJson2(ws, { type: "started", requestId: activeRequestId });
3380
3643
  });
3381
3644
  provider.on("audio", (chunk) => {
@@ -3387,7 +3650,7 @@ function handleVoiceTtsConnection(ws, store, logger, providers) {
3387
3650
  if (ws.readyState === WebSocket10.OPEN) ws.send(chunk);
3388
3651
  });
3389
3652
  provider.on("finished", () => {
3390
- if (activeStats) logger.info(statsLogFields(activeStats), "Voice TTS finished");
3653
+ if (activeStats) logger.info(statsLogFields2(activeStats), "Voice TTS finished");
3391
3654
  sendJson2(ws, { type: "finished", requestId: activeRequestId });
3392
3655
  activeRequestId = null;
3393
3656
  activeStats = null;
@@ -3395,7 +3658,7 @@ function handleVoiceTtsConnection(ws, store, logger, providers) {
3395
3658
  provider.on("error", (error) => {
3396
3659
  if (activeStats) {
3397
3660
  logger.warn(
3398
- { ...statsLogFields(activeStats), err: error },
3661
+ { ...statsLogFields2(activeStats), err: error },
3399
3662
  "Voice TTS provider reported an error"
3400
3663
  );
3401
3664
  }
@@ -3410,7 +3673,7 @@ function handleVoiceTtsConnection(ws, store, logger, providers) {
3410
3673
  provider.on("closed", (code, reason) => {
3411
3674
  if (activeStats) {
3412
3675
  logger.warn(
3413
- { ...statsLogFields(activeStats), code, reason },
3676
+ { ...statsLogFields2(activeStats), code, reason },
3414
3677
  "Voice TTS provider closed before finishing"
3415
3678
  );
3416
3679
  sendJson2(ws, {
@@ -3449,14 +3712,14 @@ function handleVoiceTtsConnection(ws, store, logger, providers) {
3449
3712
  if (!ensured) return;
3450
3713
  const { client, config } = ensured;
3451
3714
  activeRequestId = payload.requestId;
3452
- activeStats = buildStats(payload.requestId, payload.text, config);
3453
- logger.info(statsLogFields(activeStats), "Voice TTS request received");
3715
+ activeStats = buildStats2(payload.requestId, payload.text, config);
3716
+ logger.info(statsLogFields2(activeStats), "Voice TTS request received");
3454
3717
  try {
3455
3718
  client.speak(payload.text);
3456
3719
  } catch (err) {
3457
3720
  if (activeStats) {
3458
3721
  logger.warn(
3459
- { ...statsLogFields(activeStats), err },
3722
+ { ...statsLogFields2(activeStats), err },
3460
3723
  "Voice TTS request failed before provider accepted it"
3461
3724
  );
3462
3725
  }
@@ -3472,7 +3735,7 @@ function handleVoiceTtsConnection(ws, store, logger, providers) {
3472
3735
  ws.on("close", (code, reason) => {
3473
3736
  if (activeStats) {
3474
3737
  logger.warn(
3475
- { ...statsLogFields(activeStats), code, reason: closeReasonText(reason) },
3738
+ { ...statsLogFields2(activeStats), code, reason: closeReasonText(reason) },
3476
3739
  "Voice TTS client websocket closed before finishing"
3477
3740
  );
3478
3741
  }
@@ -4042,9 +4305,80 @@ var RemoteFileBridge = class {
4042
4305
  }
4043
4306
  };
4044
4307
 
4308
+ // src/web-app.ts
4309
+ import compression from "compression";
4310
+ import express from "express";
4311
+ import { existsSync as existsSync2 } from "fs";
4312
+ import { basename, join as join3 } from "path";
4313
+ var IMMUTABLE_CACHE_SECONDS = 365 * 24 * 60 * 60;
4314
+ var SHORT_CACHE_SECONDS = 60 * 60;
4315
+ var RELAY_ROUTE_ROOTS = /* @__PURE__ */ new Set([
4316
+ "/api",
4317
+ "/client",
4318
+ "/fonts",
4319
+ "/health",
4320
+ "/proxy",
4321
+ "/status",
4322
+ "/voice"
4323
+ ]);
4324
+ function isRelayRoute(pathname) {
4325
+ for (const root of RELAY_ROUTE_ROOTS) {
4326
+ if (pathname === root || pathname.startsWith(`${root}/`)) return true;
4327
+ }
4328
+ return false;
4329
+ }
4330
+ function setStaticCacheHeaders(response, filePath) {
4331
+ const fileName = basename(filePath);
4332
+ const normalizedPath = filePath.replaceAll("\\", "/");
4333
+ if (normalizedPath.includes("/assets/") || /^workbox-.+\.js$/.test(fileName)) {
4334
+ response.setHeader("Cache-Control", `public, max-age=${IMMUTABLE_CACHE_SECONDS}, immutable`);
4335
+ return;
4336
+ }
4337
+ if (fileName === "index.html" || fileName === "sw.js" || fileName === "registerSW.js" || fileName === "manifest.webmanifest") {
4338
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
4339
+ return;
4340
+ }
4341
+ response.setHeader("Cache-Control", `public, max-age=${SHORT_CACHE_SECONDS}`);
4342
+ }
4343
+ function mountWebApp(app, options) {
4344
+ const { webAssetDir, logger } = options;
4345
+ const indexPath = join3(webAssetDir, "index.html");
4346
+ if (!existsSync2(indexPath)) {
4347
+ logger.warn({ webAssetDir }, "Web assets not found; Relay will run without the Web UI");
4348
+ return false;
4349
+ }
4350
+ app.use(compression());
4351
+ app.use(
4352
+ express.static(webAssetDir, {
4353
+ index: false,
4354
+ fallthrough: true,
4355
+ setHeaders: setStaticCacheHeaders
4356
+ })
4357
+ );
4358
+ app.use((request, response, next) => {
4359
+ if (request.method !== "GET" && request.method !== "HEAD") {
4360
+ next();
4361
+ return;
4362
+ }
4363
+ if (isRelayRoute(request.path)) {
4364
+ response.status(404).json({ error: "not_found" });
4365
+ return;
4366
+ }
4367
+ if (!request.accepts("html")) {
4368
+ next();
4369
+ return;
4370
+ }
4371
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
4372
+ response.sendFile(indexPath);
4373
+ });
4374
+ logger.info({ webAssetDir }, "Web UI mounted");
4375
+ return true;
4376
+ }
4377
+
4045
4378
  // src/server.ts
4046
4379
  var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
4047
4380
  var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
4381
+ var PACKAGED_WEB_DIR = resolve(MODULE_DIR, "../assets/web");
4048
4382
  function firstHeaderValue(value) {
4049
4383
  if (Array.isArray(value)) return value[0];
4050
4384
  return value;
@@ -4099,7 +4433,8 @@ function createRelayServer(options) {
4099
4433
  "Relay chaos mode enabled"
4100
4434
  );
4101
4435
  }
4102
- const app = express();
4436
+ const app = express2();
4437
+ app.disable("x-powered-by");
4103
4438
  const fontsDir = dataDir ? `${dataDir}/fonts` : `${homedir()}/.dev-anywhere/relay-data/fonts`;
4104
4439
  const fontAssetDir = options.fontAssetDir ?? PACKAGED_FONTS_DIR;
4105
4440
  app.use(
@@ -4108,15 +4443,15 @@ function createRelayServer(options) {
4108
4443
  res.setHeader("Access-Control-Allow-Origin", "*");
4109
4444
  next();
4110
4445
  },
4111
- express.static(fontsDir, {
4446
+ express2.static(fontsDir, {
4112
4447
  maxAge: "30d",
4113
4448
  immutable: true
4114
4449
  })
4115
4450
  );
4116
- if (existsSync2(fontAssetDir)) {
4451
+ if (existsSync3(fontAssetDir)) {
4117
4452
  app.use(
4118
4453
  "/fonts",
4119
- express.static(fontAssetDir, {
4454
+ express2.static(fontAssetDir, {
4120
4455
  maxAge: "30d",
4121
4456
  immutable: true
4122
4457
  })
@@ -4137,6 +4472,10 @@ function createRelayServer(options) {
4137
4472
  app.put("/api/remote-uploads/:token", (req, res) => {
4138
4473
  remoteFileBridge.handleUploadHttpRequest(req, res);
4139
4474
  });
4475
+ const webAssetDir = options.webAssetDir === false ? void 0 : options.webAssetDir ?? PACKAGED_WEB_DIR;
4476
+ if (webAssetDir) {
4477
+ mountWebApp(app, { webAssetDir, logger });
4478
+ }
4140
4479
  const httpServer = createServer(app);
4141
4480
  const proxyWss = new WebSocketServer({ noServer: true });
4142
4481
  const clientWss = new WebSocketServer({ noServer: true });
@@ -4306,4 +4645,4 @@ export {
4306
4645
  parseRelayChaosFromEnv,
4307
4646
  createRelayServer
4308
4647
  };
4309
- //# sourceMappingURL=chunk-46NK2URF.js.map
4648
+ //# sourceMappingURL=chunk-2L3L5F4J.js.map