@dev-anywhere/relay 0.4.72 → 0.4.74

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.
@@ -513,6 +513,11 @@ var RelayClientInfoSchema = z8.object({
513
513
  connectedAt: z8.number().int().nonnegative(),
514
514
  current: z8.boolean().optional(),
515
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"]),
516
521
  remoteAddress: z8.string().optional()
517
522
  });
518
523
  var AgentCliAvailabilitySchema = z8.object({
@@ -557,7 +562,8 @@ var RequestErrorShape = {
557
562
  error: z8.string().optional(),
558
563
  errorCode: ControlErrorCodeSchema.optional()
559
564
  };
560
- var ClipboardImageMimeTypeSchema = z8.enum(["image/png", "image/jpeg", "image/webp", "image/gif"]);
565
+ var RemoteFileDispositionSchema = z8.enum(["inline", "download"]);
566
+ var RemoteFileUploadKindSchema = z8.enum(["clipboard_image", "file"]);
561
567
  function control(type, shape, directions) {
562
568
  return {
563
569
  type,
@@ -596,6 +602,82 @@ var relayControlDefinitions = [
596
602
  control("relay_client_kicked", {
597
603
  reason: z8.string().optional()
598
604
  }),
605
+ control("remote_file_url_request", {
606
+ ...RequiredRequestIdShape,
607
+ sessionId: IdSchema,
608
+ path: z8.string().min(1),
609
+ disposition: RemoteFileDispositionSchema
610
+ }),
611
+ control("remote_file_url_response", {
612
+ ...RequiredRequestIdShape,
613
+ ...RequestErrorShape,
614
+ sessionId: IdSchema,
615
+ path: z8.string().optional(),
616
+ success: z8.boolean(),
617
+ url: z8.string().optional(),
618
+ expiresAt: z8.number().int().nonnegative().optional()
619
+ }),
620
+ control("remote_file_stream_request", {
621
+ streamId: IdSchema,
622
+ sessionId: IdSchema,
623
+ path: z8.string().min(1),
624
+ disposition: RemoteFileDispositionSchema
625
+ }),
626
+ control("remote_file_stream_response", {
627
+ ...RequestErrorShape,
628
+ streamId: IdSchema,
629
+ sessionId: IdSchema,
630
+ success: z8.boolean(),
631
+ path: z8.string().optional(),
632
+ mimeType: z8.string().optional(),
633
+ size: z8.number().int().nonnegative().optional(),
634
+ fileName: z8.string().optional()
635
+ }),
636
+ control("remote_file_stream_complete", {
637
+ ...RequestErrorShape,
638
+ streamId: IdSchema,
639
+ success: z8.boolean()
640
+ }),
641
+ control("remote_file_stream_cancel", {
642
+ streamId: IdSchema
643
+ }),
644
+ control("remote_file_upload_url_request", {
645
+ ...RequiredRequestIdShape,
646
+ sessionId: IdSchema,
647
+ kind: RemoteFileUploadKindSchema,
648
+ fileName: z8.string().optional(),
649
+ mimeType: z8.string().min(1),
650
+ size: z8.number().int().nonnegative().optional()
651
+ }),
652
+ control("remote_file_upload_url_response", {
653
+ ...RequiredRequestIdShape,
654
+ ...RequestErrorShape,
655
+ sessionId: IdSchema,
656
+ success: z8.boolean(),
657
+ uploadUrl: z8.string().optional(),
658
+ expiresAt: z8.number().int().nonnegative().optional()
659
+ }),
660
+ control("remote_file_upload_stream_request", {
661
+ uploadId: IdSchema,
662
+ sessionId: IdSchema,
663
+ kind: RemoteFileUploadKindSchema,
664
+ fileName: z8.string().optional(),
665
+ mimeType: z8.string().min(1),
666
+ size: z8.number().int().nonnegative().optional()
667
+ }),
668
+ control("remote_file_upload_stream_complete", {
669
+ uploadId: IdSchema
670
+ }),
671
+ control("remote_file_upload_stream_cancel", {
672
+ uploadId: IdSchema
673
+ }),
674
+ control("remote_file_upload_stream_response", {
675
+ ...RequestErrorShape,
676
+ uploadId: IdSchema,
677
+ sessionId: IdSchema,
678
+ success: z8.boolean(),
679
+ path: z8.string().optional()
680
+ }),
599
681
  control("proxy_select", { ...RequestIdShape, proxyId: IdSchema }),
600
682
  control("proxy_select_response", {
601
683
  ...RequestIdShape,
@@ -675,7 +757,13 @@ var relayControlDefinitions = [
675
757
  control("latency_web_proxy_pong", { ...RequiredRequestIdShape, proxyNow: z8.number().optional() }, "proxy_to_client"),
676
758
  // 客户端注册协议
677
759
  control("client_register", {
678
- clientId: IdSchema
760
+ clientId: IdSchema,
761
+ userAgent: z8.string().optional(),
762
+ platform: z8.string().optional(),
763
+ maxTouchPoints: z8.number().int().nonnegative().optional(),
764
+ browserName: z8.string().min(1),
765
+ osName: z8.string().min(1),
766
+ deviceKind: z8.enum(["desktop", "tablet", "phone", "unknown"])
679
767
  }),
680
768
  control("client_register_response", {
681
769
  status: z8.enum(["restored", "proxy_offline", "new"]),
@@ -757,70 +845,6 @@ var relayControlDefinitions = [
757
845
  }, "proxy_to_client"),
758
846
  // 客户端发送到 PTY 的原始字节(ANSI 序列),不追加换行
759
847
  control("remote_input_raw", { sessionId: IdSchema, data: z8.string(), traceId: IdSchema.optional() }, "client_to_proxy"),
760
- control("clipboard_image_upload", {
761
- ...RequestIdShape,
762
- sessionId: IdSchema,
763
- mimeType: ClipboardImageMimeTypeSchema,
764
- dataBase64: z8.string().min(1),
765
- fileName: z8.string().optional()
766
- }, "client_to_proxy"),
767
- control("clipboard_image_upload_response", {
768
- ...RequestIdShape,
769
- ...RequestErrorShape,
770
- sessionId: IdSchema,
771
- success: z8.boolean(),
772
- // success=false 时 proxy 没有有效 path 可填;保持 optional 以避免占位空字符串通过校验。
773
- path: z8.string().optional()
774
- }, "proxy_to_client"),
775
- control("image_preview_request", {
776
- ...RequestIdShape,
777
- sessionId: IdSchema,
778
- path: z8.string().min(1)
779
- }, "client_to_proxy"),
780
- control("image_preview_response", {
781
- ...RequestIdShape,
782
- ...RequestErrorShape,
783
- sessionId: IdSchema,
784
- success: z8.boolean(),
785
- // 同 clipboard_image_upload_response:失败时 proxy 不一定有路径。
786
- path: z8.string().optional(),
787
- mimeType: ClipboardImageMimeTypeSchema.optional(),
788
- dataBase64: z8.string().optional(),
789
- size: z8.number().int().nonnegative().optional()
790
- }, "proxy_to_client"),
791
- // 任意文件下载: 与 image_preview 形状对称, 只是 mimeType 不限定为图片;
792
- // 单租户场景下 path 任意 (不受 previewRoots 限制), 由 proxy 端 size cap 兜底。
793
- control("file_download_request", {
794
- ...RequestIdShape,
795
- sessionId: IdSchema,
796
- path: z8.string().min(1)
797
- }, "client_to_proxy"),
798
- control("file_download_response", {
799
- ...RequestIdShape,
800
- ...RequestErrorShape,
801
- sessionId: IdSchema,
802
- success: z8.boolean(),
803
- path: z8.string().optional(),
804
- mimeType: z8.string().optional(),
805
- dataBase64: z8.string().optional(),
806
- size: z8.number().int().nonnegative().optional()
807
- }, "proxy_to_client"),
808
- // 任意文件上传: 复用 clipboard_image_upload 的形状, mimeType 放开 + fileName 必填,
809
- // 由 proxy 端写入 session cwd 的 .dev-anywhere/uploads/ 子目录, 返回相对路径供 web 拼成 @path。
810
- control("file_upload_request", {
811
- ...RequestIdShape,
812
- sessionId: IdSchema,
813
- mimeType: z8.string().min(1),
814
- dataBase64: z8.string().min(1),
815
- fileName: z8.string().min(1)
816
- }, "client_to_proxy"),
817
- control("file_upload_response", {
818
- ...RequestIdShape,
819
- ...RequestErrorShape,
820
- sessionId: IdSchema,
821
- success: z8.boolean(),
822
- path: z8.string().optional()
823
- }, "proxy_to_client"),
824
848
  // 客户端询问 proxy 的环境信息 (home 路径等), client -> proxy -> response
825
849
  // FilePathPicker 用 homePath 作为 select 模式下的默认起点, 新建会话时打开即可浏览
826
850
  control("proxy_info_request", RequestIdShape, "client_to_proxy"),
@@ -964,13 +988,50 @@ function isClientToProxyRelayControlType(type) {
964
988
 
965
989
  // ../../packages/shared/dist/constants/relay-close-codes.js
966
990
  var RelayCloseCode = {
967
- CLIENT_KICKED: 4401
991
+ CLIENT_KICKED: 4401,
992
+ CLIENT_PROTOCOL_REJECTED: 4402
968
993
  };
969
994
 
970
995
  // ../../packages/shared/dist/binary-frame.js
971
996
  var SID_LEN_BYTES = 1;
972
997
  var SEQ_BYTES = 4;
973
998
  var HEADER_FIXED_BYTES = SID_LEN_BYTES + SEQ_BYTES;
999
+ var FILE_STREAM_MARKER = 0;
1000
+ var FILE_STREAM_ID_LEN_BYTES = 1;
1001
+ var FILE_STREAM_HEADER_FIXED_BYTES = 1 + FILE_STREAM_ID_LEN_BYTES + SEQ_BYTES;
1002
+ function encodeFileStreamFrame(streamId, chunkSeq, data) {
1003
+ const streamIdBytes = new TextEncoder().encode(streamId);
1004
+ if (streamIdBytes.length === 0 || streamIdBytes.length > 255) {
1005
+ throw new RangeError(`streamId byte length must be 1-255, got ${streamIdBytes.length} (streamId=${streamId})`);
1006
+ }
1007
+ if (!Number.isInteger(chunkSeq) || chunkSeq < 0 || chunkSeq > 4294967295) {
1008
+ throw new RangeError(`chunkSeq must be a uint32, got ${chunkSeq}`);
1009
+ }
1010
+ const frame = new Uint8Array(1 + FILE_STREAM_ID_LEN_BYTES + streamIdBytes.length + SEQ_BYTES + data.length);
1011
+ frame[0] = FILE_STREAM_MARKER;
1012
+ frame[1] = streamIdBytes.length;
1013
+ frame.set(streamIdBytes, 1 + FILE_STREAM_ID_LEN_BYTES);
1014
+ const seqOffset = 1 + FILE_STREAM_ID_LEN_BYTES + streamIdBytes.length;
1015
+ new DataView(frame.buffer, frame.byteOffset + seqOffset, SEQ_BYTES).setUint32(0, chunkSeq, true);
1016
+ frame.set(data, seqOffset + SEQ_BYTES);
1017
+ return frame;
1018
+ }
1019
+ function decodeFileStreamFrame(view) {
1020
+ if (view.length < FILE_STREAM_HEADER_FIXED_BYTES)
1021
+ return null;
1022
+ if (view[0] !== FILE_STREAM_MARKER)
1023
+ return null;
1024
+ const streamIdLen = view[1];
1025
+ if (streamIdLen === 0)
1026
+ return null;
1027
+ if (view.length < 1 + FILE_STREAM_ID_LEN_BYTES + streamIdLen + SEQ_BYTES)
1028
+ return null;
1029
+ const streamId = new TextDecoder().decode(view.subarray(1 + FILE_STREAM_ID_LEN_BYTES, 1 + FILE_STREAM_ID_LEN_BYTES + streamIdLen));
1030
+ const seqOffset = 1 + FILE_STREAM_ID_LEN_BYTES + streamIdLen;
1031
+ const chunkSeq = new DataView(view.buffer, view.byteOffset + seqOffset, SEQ_BYTES).getUint32(0, true);
1032
+ const data = view.subarray(seqOffset + SEQ_BYTES);
1033
+ return { streamId, chunkSeq, data };
1034
+ }
974
1035
 
975
1036
  // ../../packages/shared/dist/state-machine.js
976
1037
  function computeAbsorbingSet(transitions) {
@@ -1173,6 +1234,11 @@ var RelayRegistry = class {
1173
1234
  connectedAt: metadata.connectedAt ?? Date.now(),
1174
1235
  ...metadata.clientId !== void 0 ? { clientId: metadata.clientId } : {},
1175
1236
  ...metadata.userAgent !== void 0 ? { userAgent: metadata.userAgent } : {},
1237
+ ...metadata.platform !== void 0 ? { platform: metadata.platform } : {},
1238
+ ...metadata.maxTouchPoints !== void 0 ? { maxTouchPoints: metadata.maxTouchPoints } : {},
1239
+ ...metadata.browserName !== void 0 ? { browserName: metadata.browserName } : {},
1240
+ ...metadata.osName !== void 0 ? { osName: metadata.osName } : {},
1241
+ ...metadata.deviceKind !== void 0 ? { deviceKind: metadata.deviceKind } : {},
1176
1242
  ...metadata.remoteAddress !== void 0 ? { remoteAddress: metadata.remoteAddress } : {}
1177
1243
  });
1178
1244
  }
@@ -1184,6 +1250,21 @@ var RelayRegistry = class {
1184
1250
  }
1185
1251
  this.addClientWs(ws, { clientId });
1186
1252
  }
1253
+ updateConnectedClientMetadata(ws, metadata) {
1254
+ const current = this.connectedClients.get(ws);
1255
+ if (!current) {
1256
+ this.addClientWs(ws, metadata);
1257
+ return;
1258
+ }
1259
+ if (metadata.clientId !== void 0) current.clientId = metadata.clientId;
1260
+ if (metadata.userAgent !== void 0) current.userAgent = metadata.userAgent;
1261
+ if (metadata.platform !== void 0) current.platform = metadata.platform;
1262
+ if (metadata.maxTouchPoints !== void 0) current.maxTouchPoints = metadata.maxTouchPoints;
1263
+ if (metadata.browserName !== void 0) current.browserName = metadata.browserName;
1264
+ if (metadata.osName !== void 0) current.osName = metadata.osName;
1265
+ if (metadata.deviceKind !== void 0) current.deviceKind = metadata.deviceKind;
1266
+ if (metadata.remoteAddress !== void 0) current.remoteAddress = metadata.remoteAddress;
1267
+ }
1187
1268
  removeClientWs(ws) {
1188
1269
  this.connectedClients.delete(ws);
1189
1270
  }
@@ -1235,6 +1316,7 @@ var RelayRegistry = class {
1235
1316
  const details = [];
1236
1317
  for (const [ws, metadata] of this.connectedClients) {
1237
1318
  if (ws.readyState !== WebSocket.OPEN || !metadata.clientId) continue;
1319
+ if (!metadata.browserName || !metadata.osName || !metadata.deviceKind) continue;
1238
1320
  const binding = this.clientBindings.get(metadata.clientId);
1239
1321
  details.push({
1240
1322
  clientId: metadata.clientId,
@@ -1242,6 +1324,11 @@ var RelayRegistry = class {
1242
1324
  connectedAt: metadata.connectedAt,
1243
1325
  ...metadata.clientId === currentClientId ? { current: true } : {},
1244
1326
  ...metadata.userAgent !== void 0 ? { userAgent: metadata.userAgent } : {},
1327
+ ...metadata.platform !== void 0 ? { platform: metadata.platform } : {},
1328
+ ...metadata.maxTouchPoints !== void 0 ? { maxTouchPoints: metadata.maxTouchPoints } : {},
1329
+ browserName: metadata.browserName,
1330
+ osName: metadata.osName,
1331
+ deviceKind: metadata.deviceKind,
1245
1332
  ...metadata.remoteAddress !== void 0 ? { remoteAddress: metadata.remoteAddress } : {}
1246
1333
  });
1247
1334
  }
@@ -1574,7 +1661,7 @@ function rejectNotRegistered(ws) {
1574
1661
  })
1575
1662
  );
1576
1663
  }
1577
- function handleProxyConnection(ws, registry, logger, chaos) {
1664
+ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1578
1665
  const proxyWs = ws;
1579
1666
  proxyWs.isAlive = true;
1580
1667
  proxyWs.on("pong", () => {
@@ -1586,6 +1673,9 @@ function handleProxyConnection(ws, registry, logger, chaos) {
1586
1673
  logger.warn({ size: data.length }, "Binary frame rejected: invalid size");
1587
1674
  return;
1588
1675
  }
1676
+ if (proxyWs.proxyId && remoteFileBridge?.handleProxyBinary(proxyWs.proxyId, data)) {
1677
+ return;
1678
+ }
1589
1679
  const sessionIdLen = data[0];
1590
1680
  if (sessionIdLen === 0 || sessionIdLen > 255 || data.length < 1 + sessionIdLen) {
1591
1681
  logger.warn(
@@ -1674,6 +1764,18 @@ function handleProxyConnection(ws, registry, logger, chaos) {
1674
1764
  return;
1675
1765
  }
1676
1766
  if (result.kind === "control") {
1767
+ if (proxyWs.proxyId && result.message.type === "remote_file_stream_response") {
1768
+ remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1769
+ return;
1770
+ }
1771
+ if (proxyWs.proxyId && result.message.type === "remote_file_stream_complete") {
1772
+ remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1773
+ return;
1774
+ }
1775
+ if (proxyWs.proxyId && result.message.type === "remote_file_upload_stream_response") {
1776
+ remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1777
+ return;
1778
+ }
1677
1779
  if (isProxyToClientRelayControlType(result.message.type)) {
1678
1780
  if (!proxyWs.proxyId) {
1679
1781
  rejectNotRegistered(proxyWs);
@@ -1754,7 +1856,6 @@ function handleProxyConnection(ws, registry, logger, chaos) {
1754
1856
 
1755
1857
  // src/handlers/client.ts
1756
1858
  import { WebSocket as WebSocket7 } from "ws";
1757
- import { nanoid as nanoid3 } from "nanoid";
1758
1859
 
1759
1860
  // src/voice/bailian-asr.ts
1760
1861
  import { EventEmitter } from "events";
@@ -2404,9 +2505,26 @@ function handleVoiceConfigControl(msg, ws, store, logger, providers) {
2404
2505
 
2405
2506
  // src/handlers/client.ts
2406
2507
  var MAX_JSON_MESSAGE_SIZE2 = 1 * 1024 * 1024;
2407
- function handleClientRegister(clientId, clientWs, registry, logger) {
2508
+ function isMalformedClientRegister(raw) {
2509
+ try {
2510
+ const parsed = JSON.parse(raw);
2511
+ return parsed !== null && typeof parsed === "object" && parsed.type === "client_register";
2512
+ } catch {
2513
+ return false;
2514
+ }
2515
+ }
2516
+ function handleClientRegister(registration, clientWs, registry, logger) {
2517
+ const { clientId } = registration;
2408
2518
  clientWs.clientId = clientId;
2409
- registry.updateConnectedClientId(clientWs, clientId);
2519
+ registry.updateConnectedClientMetadata(clientWs, {
2520
+ clientId,
2521
+ ...registration.userAgent !== void 0 ? { userAgent: registration.userAgent } : {},
2522
+ ...registration.platform !== void 0 ? { platform: registration.platform } : {},
2523
+ ...registration.maxTouchPoints !== void 0 ? { maxTouchPoints: registration.maxTouchPoints } : {},
2524
+ browserName: registration.browserName,
2525
+ osName: registration.osName,
2526
+ deviceKind: registration.deviceKind
2527
+ });
2410
2528
  const binding = registry.getClientBinding(clientId);
2411
2529
  if (!binding) {
2412
2530
  clientWs.send(
@@ -2516,6 +2634,19 @@ function rejectNotBound(ws) {
2516
2634
  })
2517
2635
  );
2518
2636
  }
2637
+ function rejectNotRegistered2(ws, requestId) {
2638
+ ws.send(
2639
+ JSON.stringify({
2640
+ type: "relay_error",
2641
+ requestId,
2642
+ code: RelayErrorCode.NOT_REGISTERED,
2643
+ message: "Client must register before selecting a proxy"
2644
+ })
2645
+ );
2646
+ }
2647
+ function closeRejectedClientProtocol(ws) {
2648
+ ws.close(RelayCloseCode.CLIENT_PROTOCOL_REJECTED, "client protocol rejected");
2649
+ }
2519
2650
  function rejectProxySelect(ws, requestId, proxyId) {
2520
2651
  ws.send(
2521
2652
  JSON.stringify({
@@ -2543,7 +2674,31 @@ function sendRelayProxyProbeFailure(ws, requestId, error, chaos) {
2543
2674
  }
2544
2675
  ws.send(response);
2545
2676
  }
2546
- function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, voiceProviders, connectionInfo = {}) {
2677
+ function sendRemoteFileUrlFailure(ws, requestId, sessionId, error, errorCode = ControlErrorCode.UNKNOWN) {
2678
+ ws.send(
2679
+ serializeControl({
2680
+ type: "remote_file_url_response",
2681
+ requestId,
2682
+ sessionId,
2683
+ success: false,
2684
+ error,
2685
+ errorCode
2686
+ })
2687
+ );
2688
+ }
2689
+ function sendRemoteFileUploadUrlFailure(ws, requestId, sessionId, error, errorCode = ControlErrorCode.UNKNOWN) {
2690
+ ws.send(
2691
+ serializeControl({
2692
+ type: "remote_file_upload_url_response",
2693
+ requestId,
2694
+ sessionId,
2695
+ success: false,
2696
+ error,
2697
+ errorCode
2698
+ })
2699
+ );
2700
+ }
2701
+ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
2547
2702
  const clientWs = ws;
2548
2703
  clientWs.isAlive = true;
2549
2704
  registry.addClientWs(clientWs, connectionInfo);
@@ -2570,7 +2725,7 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2570
2725
  "Client message received"
2571
2726
  );
2572
2727
  if (msg.type === "client_register") {
2573
- handleClientRegister(msg.clientId, clientWs, registry, logger);
2728
+ handleClientRegister(msg, clientWs, registry, logger);
2574
2729
  return;
2575
2730
  }
2576
2731
  if (msg.type === "relay_client_list_request") {
@@ -2581,6 +2736,115 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2581
2736
  handleRelayClientKick(clientWs, registry, logger, msg.requestId, msg.clientId);
2582
2737
  return;
2583
2738
  }
2739
+ if (msg.type === "remote_file_url_request") {
2740
+ if (!remoteFileBridge) {
2741
+ sendRemoteFileUrlFailure(clientWs, msg.requestId, msg.sessionId, "\u6587\u4EF6\u6D41\u670D\u52A1\u4E0D\u53EF\u7528");
2742
+ return;
2743
+ }
2744
+ if (!clientWs.clientId) {
2745
+ sendRemoteFileUrlFailure(
2746
+ clientWs,
2747
+ msg.requestId,
2748
+ msg.sessionId,
2749
+ "\u5BA2\u6237\u7AEF\u672A\u6CE8\u518C",
2750
+ ControlErrorCode.UNKNOWN
2751
+ );
2752
+ return;
2753
+ }
2754
+ const targetProxyId = clientWs.boundProxyId;
2755
+ if (!targetProxyId || !registry.isProxyOnline(targetProxyId)) {
2756
+ sendRemoteFileUrlFailure(
2757
+ clientWs,
2758
+ msg.requestId,
2759
+ msg.sessionId,
2760
+ "\u5F53\u524D\u672A\u8FDE\u63A5\u5F00\u53D1\u673A",
2761
+ ControlErrorCode.PROXY_OFFLINE
2762
+ );
2763
+ return;
2764
+ }
2765
+ const ownerProxyId = registry.getProxyForSession(msg.sessionId);
2766
+ if (ownerProxyId && ownerProxyId !== targetProxyId) {
2767
+ sendRemoteFileUrlFailure(
2768
+ clientWs,
2769
+ msg.requestId,
2770
+ msg.sessionId,
2771
+ "\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u5F53\u524D\u5F00\u53D1\u673A",
2772
+ ControlErrorCode.SESSION_NOT_FOUND
2773
+ );
2774
+ return;
2775
+ }
2776
+ const { url, expiresAt } = remoteFileBridge.createUrl({
2777
+ clientId: clientWs.clientId,
2778
+ proxyId: targetProxyId,
2779
+ sessionId: msg.sessionId,
2780
+ path: msg.path,
2781
+ disposition: msg.disposition
2782
+ });
2783
+ clientWs.send(
2784
+ serializeControl({
2785
+ type: "remote_file_url_response",
2786
+ requestId: msg.requestId,
2787
+ sessionId: msg.sessionId,
2788
+ path: msg.path,
2789
+ success: true,
2790
+ url,
2791
+ expiresAt
2792
+ })
2793
+ );
2794
+ return;
2795
+ }
2796
+ if (msg.type === "remote_file_upload_url_request") {
2797
+ if (!remoteFileBridge) {
2798
+ sendRemoteFileUploadUrlFailure(clientWs, msg.requestId, msg.sessionId, "\u4E0A\u4F20\u670D\u52A1\u4E0D\u53EF\u7528");
2799
+ return;
2800
+ }
2801
+ if (!clientWs.clientId) {
2802
+ sendRemoteFileUploadUrlFailure(clientWs, msg.requestId, msg.sessionId, "\u5BA2\u6237\u7AEF\u672A\u6CE8\u518C");
2803
+ return;
2804
+ }
2805
+ const targetProxyId = clientWs.boundProxyId;
2806
+ if (!targetProxyId || !registry.isProxyOnline(targetProxyId)) {
2807
+ sendRemoteFileUploadUrlFailure(
2808
+ clientWs,
2809
+ msg.requestId,
2810
+ msg.sessionId,
2811
+ "\u5F53\u524D\u672A\u8FDE\u63A5\u5F00\u53D1\u673A",
2812
+ ControlErrorCode.PROXY_OFFLINE
2813
+ );
2814
+ return;
2815
+ }
2816
+ const ownerProxyId = registry.getProxyForSession(msg.sessionId);
2817
+ if (ownerProxyId && ownerProxyId !== targetProxyId) {
2818
+ sendRemoteFileUploadUrlFailure(
2819
+ clientWs,
2820
+ msg.requestId,
2821
+ msg.sessionId,
2822
+ "\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u5F53\u524D\u5F00\u53D1\u673A",
2823
+ ControlErrorCode.SESSION_NOT_FOUND
2824
+ );
2825
+ return;
2826
+ }
2827
+ const { uploadUrl, expiresAt } = remoteFileBridge.createUploadUrl({
2828
+ clientId: clientWs.clientId,
2829
+ proxyId: targetProxyId,
2830
+ sessionId: msg.sessionId,
2831
+ kind: msg.kind,
2832
+ fileName: msg.fileName,
2833
+ mimeType: msg.mimeType,
2834
+ size: msg.size
2835
+ });
2836
+ clientWs.send(
2837
+ serializeControl({
2838
+ type: "remote_file_upload_url_response",
2839
+ requestId: msg.requestId,
2840
+ sessionId: msg.sessionId,
2841
+ success: true,
2842
+ uploadUrl,
2843
+ expiresAt
2844
+ })
2845
+ );
2846
+ return;
2847
+ }
2584
2848
  if (msg.type === "proxy_list_request") {
2585
2849
  const proxies = registry.listProxiesWithName().map((p) => ({
2586
2850
  ...p,
@@ -2668,13 +2932,15 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2668
2932
  return;
2669
2933
  }
2670
2934
  if (msg.type === "proxy_select") {
2935
+ if (!clientWs.clientId) {
2936
+ rejectNotRegistered2(clientWs, msg.requestId);
2937
+ closeRejectedClientProtocol(clientWs);
2938
+ return;
2939
+ }
2671
2940
  if (!registry.isProxyOnline(msg.proxyId)) {
2672
2941
  rejectProxySelect(clientWs, msg.requestId, msg.proxyId);
2673
2942
  return;
2674
2943
  }
2675
- if (!clientWs.clientId) {
2676
- clientWs.clientId = `anon-${nanoid3(10)}`;
2677
- }
2678
2944
  const bound = registry.bindClientById(clientWs.clientId, msg.proxyId, clientWs);
2679
2945
  if (!bound) {
2680
2946
  rejectProxySelect(clientWs, msg.requestId, msg.proxyId);
@@ -2723,6 +2989,9 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2723
2989
  message: `${result.error} | raw: ${raw.slice(0, 200)}`
2724
2990
  })
2725
2991
  );
2992
+ if (isMalformedClientRegister(raw)) {
2993
+ closeRejectedClientProtocol(clientWs);
2994
+ }
2726
2995
  });
2727
2996
  clientWs.on("close", () => {
2728
2997
  registry.removeClientWs(clientWs);
@@ -3295,6 +3564,348 @@ function createVoiceProviderRegistry(adapters) {
3295
3564
  };
3296
3565
  }
3297
3566
 
3567
+ // src/remote-file-bridge.ts
3568
+ import { nanoid as nanoid3 } from "nanoid";
3569
+ import { WebSocket as WebSocket11 } from "ws";
3570
+ var REMOTE_FILE_TOKEN_TTL_MS = 5 * 60 * 1e3;
3571
+ var REMOTE_FILE_METADATA_TIMEOUT_MS = 30 * 1e3;
3572
+ var REMOTE_FILE_UPLOAD_RESPONSE_TIMEOUT_MS = 60 * 1e3;
3573
+ function encodeContentDisposition(disposition, fileName) {
3574
+ const type = disposition === "download" ? "attachment" : "inline";
3575
+ const fallback = fileName.replace(/["\\\r\n]/g, "_") || "download";
3576
+ return `${type}; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
3577
+ }
3578
+ function statusForErrorCode(errorCode) {
3579
+ switch (errorCode) {
3580
+ case ControlErrorCode.SESSION_NOT_FOUND:
3581
+ return 404;
3582
+ case ControlErrorCode.INVALID_PATH:
3583
+ return 400;
3584
+ case ControlErrorCode.PATH_ACCESS_DENIED:
3585
+ return 403;
3586
+ default:
3587
+ return 500;
3588
+ }
3589
+ }
3590
+ var RemoteFileBridge = class {
3591
+ constructor(deps) {
3592
+ this.deps = deps;
3593
+ }
3594
+ deps;
3595
+ tokens = /* @__PURE__ */ new Map();
3596
+ uploadTokens = /* @__PURE__ */ new Map();
3597
+ pendingStreams = /* @__PURE__ */ new Map();
3598
+ pendingUploads = /* @__PURE__ */ new Map();
3599
+ createUrl(input) {
3600
+ this.cleanupExpiredTokens();
3601
+ const token = nanoid3(32);
3602
+ const expiresAt = Date.now() + REMOTE_FILE_TOKEN_TTL_MS;
3603
+ this.tokens.set(token, {
3604
+ token,
3605
+ clientId: input.clientId,
3606
+ proxyId: input.proxyId,
3607
+ sessionId: input.sessionId,
3608
+ path: input.path,
3609
+ disposition: input.disposition,
3610
+ expiresAt
3611
+ });
3612
+ return { url: `/api/remote-files/${token}`, expiresAt };
3613
+ }
3614
+ createUploadUrl(input) {
3615
+ this.cleanupExpiredTokens();
3616
+ const token = nanoid3(32);
3617
+ const expiresAt = Date.now() + REMOTE_FILE_TOKEN_TTL_MS;
3618
+ this.uploadTokens.set(token, {
3619
+ token,
3620
+ clientId: input.clientId,
3621
+ proxyId: input.proxyId,
3622
+ sessionId: input.sessionId,
3623
+ kind: input.kind,
3624
+ fileName: input.fileName,
3625
+ mimeType: input.mimeType,
3626
+ size: input.size,
3627
+ expiresAt
3628
+ });
3629
+ return { uploadUrl: `/api/remote-uploads/${token}`, expiresAt };
3630
+ }
3631
+ handleHttpRequest(req, res) {
3632
+ const tokenValue = req.params.token;
3633
+ const token = typeof tokenValue === "string" ? this.tokens.get(tokenValue) : void 0;
3634
+ if (!token || token.expiresAt <= Date.now()) {
3635
+ if (typeof tokenValue === "string") this.tokens.delete(tokenValue);
3636
+ res.status(404).json({ error: "remote_file_url_expired" });
3637
+ return;
3638
+ }
3639
+ const proxyWs = this.deps.registry.getProxy(token.proxyId);
3640
+ if (!proxyWs || proxyWs.readyState !== WebSocket11.OPEN) {
3641
+ res.status(502).json({ error: "proxy_offline" });
3642
+ return;
3643
+ }
3644
+ const streamId = nanoid3(21);
3645
+ const metadataTimer = setTimeout(() => {
3646
+ this.failStream(streamId, 504, "\u8BFB\u53D6\u6587\u4EF6\u5143\u6570\u636E\u8D85\u65F6");
3647
+ }, REMOTE_FILE_METADATA_TIMEOUT_MS);
3648
+ const pending = {
3649
+ streamId,
3650
+ token,
3651
+ res,
3652
+ metadataTimer,
3653
+ headersSent: false,
3654
+ finished: false
3655
+ };
3656
+ this.pendingStreams.set(streamId, pending);
3657
+ res.on("close", () => {
3658
+ if (pending.finished) return;
3659
+ pending.finished = true;
3660
+ this.pendingStreams.delete(streamId);
3661
+ clearTimeout(metadataTimer);
3662
+ this.sendCancel(token.proxyId, streamId);
3663
+ this.deps.logger.info({ streamId, proxyId: token.proxyId }, "Remote file HTTP stream closed");
3664
+ });
3665
+ proxyWs.send(
3666
+ serializeControl({
3667
+ type: "remote_file_stream_request",
3668
+ streamId,
3669
+ sessionId: token.sessionId,
3670
+ path: token.path,
3671
+ disposition: token.disposition
3672
+ })
3673
+ );
3674
+ this.deps.logger.info(
3675
+ { streamId, proxyId: token.proxyId, sessionId: token.sessionId, path: token.path },
3676
+ "Remote file HTTP stream requested"
3677
+ );
3678
+ }
3679
+ handleUploadHttpRequest(req, res) {
3680
+ const tokenValue = req.params.token;
3681
+ const token = typeof tokenValue === "string" ? this.uploadTokens.get(tokenValue) : void 0;
3682
+ if (!token || token.expiresAt <= Date.now()) {
3683
+ if (typeof tokenValue === "string") this.uploadTokens.delete(tokenValue);
3684
+ res.status(404).json({ error: "remote_file_upload_url_expired" });
3685
+ return;
3686
+ }
3687
+ const proxyWs = this.deps.registry.getProxy(token.proxyId);
3688
+ if (!proxyWs || proxyWs.readyState !== WebSocket11.OPEN) {
3689
+ res.status(502).json({ error: "proxy_offline" });
3690
+ return;
3691
+ }
3692
+ const uploadId = nanoid3(21);
3693
+ const pending = {
3694
+ uploadId,
3695
+ token,
3696
+ res,
3697
+ chunkSeq: 0,
3698
+ requestEnded: false,
3699
+ finished: false,
3700
+ responseTimer: null
3701
+ };
3702
+ this.pendingUploads.set(uploadId, pending);
3703
+ proxyWs.send(
3704
+ serializeControl({
3705
+ type: "remote_file_upload_stream_request",
3706
+ uploadId,
3707
+ sessionId: token.sessionId,
3708
+ kind: token.kind,
3709
+ mimeType: token.mimeType,
3710
+ ...token.fileName ? { fileName: token.fileName } : {},
3711
+ ...token.size !== void 0 ? { size: token.size } : {}
3712
+ })
3713
+ );
3714
+ req.on("data", (chunk) => {
3715
+ if (pending.finished) return;
3716
+ req.pause();
3717
+ const frame = encodeFileStreamFrame(uploadId, pending.chunkSeq, Buffer.from(chunk));
3718
+ pending.chunkSeq += 1;
3719
+ proxyWs.send(frame, { binary: true }, (err) => {
3720
+ if (err) {
3721
+ this.failUpload(uploadId, 502, err.message);
3722
+ return;
3723
+ }
3724
+ req.resume();
3725
+ });
3726
+ });
3727
+ req.on("end", () => {
3728
+ if (pending.finished) return;
3729
+ pending.requestEnded = true;
3730
+ pending.responseTimer = setTimeout(() => {
3731
+ this.failUpload(uploadId, 504, "\u7B49\u5F85\u4E0A\u4F20\u5199\u5165\u7ED3\u679C\u8D85\u65F6");
3732
+ }, REMOTE_FILE_UPLOAD_RESPONSE_TIMEOUT_MS);
3733
+ proxyWs.send(serializeControl({ type: "remote_file_upload_stream_complete", uploadId }));
3734
+ });
3735
+ req.on("error", (err) => {
3736
+ this.failUpload(uploadId, 400, err.message);
3737
+ });
3738
+ req.on("close", () => {
3739
+ if (pending.finished || pending.requestEnded) return;
3740
+ this.failUpload(uploadId, 499, "\u4E0A\u4F20\u8BF7\u6C42\u5DF2\u65AD\u5F00");
3741
+ });
3742
+ this.deps.logger.info(
3743
+ { uploadId, proxyId: token.proxyId, sessionId: token.sessionId, kind: token.kind },
3744
+ "Remote file HTTP upload requested"
3745
+ );
3746
+ }
3747
+ handleProxyControl(proxyId, msg) {
3748
+ if (msg.type === "remote_file_upload_stream_response") {
3749
+ return this.handleUploadResponse(proxyId, msg);
3750
+ }
3751
+ const pending = this.pendingStreams.get(msg.streamId);
3752
+ if (!pending) return true;
3753
+ if (pending.token.proxyId !== proxyId) {
3754
+ this.deps.logger.warn(
3755
+ { streamId: msg.streamId, expectedProxyId: pending.token.proxyId, proxyId },
3756
+ "Remote file stream control ignored: proxy mismatch"
3757
+ );
3758
+ return true;
3759
+ }
3760
+ if (msg.type === "remote_file_stream_response") {
3761
+ this.handleStreamResponse(pending, msg);
3762
+ return true;
3763
+ }
3764
+ this.handleStreamComplete(pending, msg);
3765
+ return true;
3766
+ }
3767
+ handleProxyBinary(proxyId, data) {
3768
+ const decoded = decodeFileStreamFrame(data);
3769
+ if (!decoded) return false;
3770
+ const pending = this.pendingStreams.get(decoded.streamId);
3771
+ if (!pending) return true;
3772
+ if (pending.token.proxyId !== proxyId) {
3773
+ this.deps.logger.warn(
3774
+ { streamId: decoded.streamId, expectedProxyId: pending.token.proxyId, proxyId },
3775
+ "Remote file stream chunk ignored: proxy mismatch"
3776
+ );
3777
+ return true;
3778
+ }
3779
+ if (!pending.headersSent || pending.finished) return true;
3780
+ pending.res.write(Buffer.from(decoded.data));
3781
+ return true;
3782
+ }
3783
+ handleStreamResponse(pending, msg) {
3784
+ clearTimeout(pending.metadataTimer);
3785
+ if (!msg.success) {
3786
+ this.failStream(
3787
+ pending.streamId,
3788
+ statusForErrorCode(msg.errorCode),
3789
+ msg.error ?? "\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25",
3790
+ msg.errorCode
3791
+ );
3792
+ return;
3793
+ }
3794
+ pending.headersSent = true;
3795
+ pending.res.status(200);
3796
+ pending.res.setHeader("Content-Type", msg.mimeType ?? "application/octet-stream");
3797
+ pending.res.setHeader("Cache-Control", "no-store");
3798
+ pending.res.setHeader("X-Content-Type-Options", "nosniff");
3799
+ if (msg.size !== void 0) {
3800
+ pending.res.setHeader("Content-Length", String(msg.size));
3801
+ }
3802
+ pending.res.setHeader(
3803
+ "Content-Disposition",
3804
+ encodeContentDisposition(pending.token.disposition, msg.fileName ?? "download")
3805
+ );
3806
+ pending.res.flushHeaders();
3807
+ }
3808
+ handleStreamComplete(pending, msg) {
3809
+ clearTimeout(pending.metadataTimer);
3810
+ pending.finished = true;
3811
+ this.pendingStreams.delete(pending.streamId);
3812
+ if (!msg.success) {
3813
+ if (!pending.headersSent) {
3814
+ this.writeJsonError(
3815
+ pending.res,
3816
+ statusForErrorCode(msg.errorCode),
3817
+ msg.error ?? "\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25",
3818
+ msg.errorCode
3819
+ );
3820
+ } else {
3821
+ pending.res.destroy(new Error(msg.error ?? "Remote file stream failed"));
3822
+ }
3823
+ return;
3824
+ }
3825
+ pending.res.end();
3826
+ }
3827
+ handleUploadResponse(proxyId, msg) {
3828
+ const pending = this.pendingUploads.get(msg.uploadId);
3829
+ if (!pending) return true;
3830
+ if (pending.token.proxyId !== proxyId) {
3831
+ this.deps.logger.warn(
3832
+ { uploadId: msg.uploadId, expectedProxyId: pending.token.proxyId, proxyId },
3833
+ "Remote file upload response ignored: proxy mismatch"
3834
+ );
3835
+ return true;
3836
+ }
3837
+ pending.finished = true;
3838
+ this.pendingUploads.delete(msg.uploadId);
3839
+ if (pending.responseTimer) clearTimeout(pending.responseTimer);
3840
+ if (!msg.success) {
3841
+ this.writeJsonError(
3842
+ pending.res,
3843
+ statusForErrorCode(msg.errorCode),
3844
+ msg.error ?? "\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25",
3845
+ msg.errorCode
3846
+ );
3847
+ return true;
3848
+ }
3849
+ pending.res.status(200).json({
3850
+ sessionId: msg.sessionId,
3851
+ success: true,
3852
+ path: msg.path
3853
+ });
3854
+ return true;
3855
+ }
3856
+ failStream(streamId, status, error, errorCode) {
3857
+ const pending = this.pendingStreams.get(streamId);
3858
+ if (!pending) return;
3859
+ pending.finished = true;
3860
+ this.pendingStreams.delete(streamId);
3861
+ clearTimeout(pending.metadataTimer);
3862
+ this.sendCancel(pending.token.proxyId, streamId);
3863
+ if (!pending.headersSent) {
3864
+ this.writeJsonError(pending.res, status, error, errorCode);
3865
+ } else {
3866
+ pending.res.destroy(new Error(error));
3867
+ }
3868
+ }
3869
+ failUpload(uploadId, status, error, errorCode) {
3870
+ const pending = this.pendingUploads.get(uploadId);
3871
+ if (!pending) return;
3872
+ pending.finished = true;
3873
+ this.pendingUploads.delete(uploadId);
3874
+ if (pending.responseTimer) clearTimeout(pending.responseTimer);
3875
+ this.sendUploadCancel(pending.token.proxyId, uploadId);
3876
+ if (!pending.res.headersSent) {
3877
+ this.writeJsonError(pending.res, status, error, errorCode);
3878
+ } else {
3879
+ pending.res.destroy(new Error(error));
3880
+ }
3881
+ }
3882
+ writeJsonError(res, status, error, errorCode) {
3883
+ res.status(status).json({
3884
+ error,
3885
+ ...errorCode ? { errorCode } : {}
3886
+ });
3887
+ }
3888
+ sendCancel(proxyId, streamId) {
3889
+ const proxyWs = this.deps.registry.getProxy(proxyId);
3890
+ if (!proxyWs || proxyWs.readyState !== WebSocket11.OPEN) return;
3891
+ proxyWs.send(serializeControl({ type: "remote_file_stream_cancel", streamId }));
3892
+ }
3893
+ sendUploadCancel(proxyId, uploadId) {
3894
+ const proxyWs = this.deps.registry.getProxy(proxyId);
3895
+ if (!proxyWs || proxyWs.readyState !== WebSocket11.OPEN) return;
3896
+ proxyWs.send(serializeControl({ type: "remote_file_upload_stream_cancel", uploadId }));
3897
+ }
3898
+ cleanupExpiredTokens() {
3899
+ const now = Date.now();
3900
+ for (const [token, meta] of this.tokens) {
3901
+ if (meta.expiresAt <= now) this.tokens.delete(token);
3902
+ }
3903
+ for (const [token, meta] of this.uploadTokens) {
3904
+ if (meta.expiresAt <= now) this.uploadTokens.delete(token);
3905
+ }
3906
+ }
3907
+ };
3908
+
3298
3909
  // src/server.ts
3299
3910
  var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
3300
3911
  var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
@@ -3327,6 +3938,7 @@ function createRelayServer(options) {
3327
3938
  );
3328
3939
  }
3329
3940
  const registry = new RelayRegistry();
3941
+ const remoteFileBridge = new RemoteFileBridge({ registry, logger });
3330
3942
  const voiceConfigStore = createVoiceConfigStore({
3331
3943
  dataDir,
3332
3944
  defaults: options.voiceDefaults
@@ -3383,6 +3995,12 @@ function createRelayServer(options) {
3383
3995
  getClientToken: () => clientToken ?? null
3384
3996
  })
3385
3997
  );
3998
+ app.get("/api/remote-files/:token", (req, res) => {
3999
+ remoteFileBridge.handleHttpRequest(req, res);
4000
+ });
4001
+ app.put("/api/remote-uploads/:token", (req, res) => {
4002
+ remoteFileBridge.handleUploadHttpRequest(req, res);
4003
+ });
3386
4004
  const httpServer = createServer(app);
3387
4005
  const proxyWss = new WebSocketServer({ noServer: true });
3388
4006
  const clientWss = new WebSocketServer({ noServer: true });
@@ -3450,13 +4068,22 @@ function createRelayServer(options) {
3450
4068
  socket.destroy();
3451
4069
  });
3452
4070
  proxyWss.on("connection", (ws) => {
3453
- handleProxyConnection(ws, registry, logger, relayChaos);
4071
+ handleProxyConnection(ws, registry, logger, relayChaos, remoteFileBridge);
3454
4072
  });
3455
4073
  clientWss.on("connection", (ws, request) => {
3456
- handleClientConnection(ws, registry, logger, relayChaos, voiceConfigStore, voiceProviders, {
3457
- userAgent: firstHeaderValue(request.headers["user-agent"]),
3458
- remoteAddress: requestRemoteAddress(request)
3459
- });
4074
+ handleClientConnection(
4075
+ ws,
4076
+ registry,
4077
+ logger,
4078
+ relayChaos,
4079
+ voiceConfigStore,
4080
+ voiceProviders,
4081
+ remoteFileBridge,
4082
+ {
4083
+ userAgent: firstHeaderValue(request.headers["user-agent"]),
4084
+ remoteAddress: requestRemoteAddress(request)
4085
+ }
4086
+ );
3460
4087
  });
3461
4088
  voiceAsrWss.on("connection", (ws) => {
3462
4089
  handleVoiceAsrConnection(ws, voiceConfigStore, logger, voiceProviders);
@@ -3526,4 +4153,4 @@ export {
3526
4153
  parseRelayChaosFromEnv,
3527
4154
  createRelayServer
3528
4155
  };
3529
- //# sourceMappingURL=chunk-H7OL7UK7.js.map
4156
+ //# sourceMappingURL=chunk-XY4ATTV2.js.map