@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.
- package/dist/{chunk-H7OL7UK7.js → chunk-XY4ATTV2.js} +709 -82
- package/dist/chunk-XY4ATTV2.js.map +1 -0
- package/dist/handlers/client.d.ts +2 -1
- package/dist/handlers/client.d.ts.map +1 -1
- package/dist/handlers/proxy.d.ts +2 -1
- package/dist/handlers/proxy.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/registry.d.ts +11 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/remote-file-bridge.d.ts +57 -0
- package/dist/remote-file-bridge.d.ts.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-H7OL7UK7.js.map +0 -1
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
3457
|
-
|
|
3458
|
-
|
|
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-
|
|
4156
|
+
//# sourceMappingURL=chunk-XY4ATTV2.js.map
|