@dev-anywhere/relay 0.5.46 → 0.5.47
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/assets/web/assets/{fvad-kT2VXipe.js → fvad-B51vkASP.js} +1 -1
- package/assets/web/assets/index-BCWFUjda.js +797 -0
- package/assets/web/assets/index-mGSY150B.css +41 -0
- package/assets/web/index.html +2 -2
- package/assets/web/sw.js +1 -1
- package/dist/{chunk-7KWO37WD.js → chunk-4Q4EAZP6.js} +223 -10
- package/dist/chunk-4Q4EAZP6.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/pty-snapshot-route-registry.d.ts +41 -0
- package/dist/pty-snapshot-route-registry.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/assets/web/assets/index-Bh_VIVxk.css +0 -41
- package/assets/web/assets/index-DVUEh9GK.js +0 -797
- package/dist/chunk-7KWO37WD.js.map +0 -1
|
@@ -1045,7 +1045,7 @@ var relayControlDefinitions = [
|
|
|
1045
1045
|
}))
|
|
1046
1046
|
}),
|
|
1047
1047
|
// PTY 会话订阅,client -> proxy,触发 terminal serialize() 返回当前状态
|
|
1048
|
-
control("session_subscribe", { sessionId: IdSchema, requestId: IdSchema
|
|
1048
|
+
control("session_subscribe", { sessionId: IdSchema, requestId: IdSchema }, "client_to_proxy"),
|
|
1049
1049
|
// PTY 会话快照,proxy -> client,serialize() 的全量终端状态
|
|
1050
1050
|
control("session_snapshot", {
|
|
1051
1051
|
sessionId: IdSchema,
|
|
@@ -1053,7 +1053,7 @@ var relayControlDefinitions = [
|
|
|
1053
1053
|
rows: z9.number().int().positive(),
|
|
1054
1054
|
data: z9.string(),
|
|
1055
1055
|
outputSeq: z9.number().int().nonnegative(),
|
|
1056
|
-
requestId: IdSchema
|
|
1056
|
+
requestId: IdSchema
|
|
1057
1057
|
}, "proxy_to_client")
|
|
1058
1058
|
];
|
|
1059
1059
|
var relayControlSchemas = relayControlDefinitions.map((definition) => definition.schema);
|
|
@@ -1776,7 +1776,7 @@ function rejectNotRegistered(ws) {
|
|
|
1776
1776
|
})
|
|
1777
1777
|
);
|
|
1778
1778
|
}
|
|
1779
|
-
function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
|
|
1779
|
+
function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, chaos, remoteFileBridge) {
|
|
1780
1780
|
const proxyWs = ws;
|
|
1781
1781
|
proxyWs.isAlive = true;
|
|
1782
1782
|
proxyWs.on("pong", () => {
|
|
@@ -1806,7 +1806,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
|
|
|
1806
1806
|
const clients = registry.getClientsForProxy(proxyWs.proxyId);
|
|
1807
1807
|
for (const clientWs of clients) {
|
|
1808
1808
|
if (clientWs.readyState === WebSocket4.OPEN) {
|
|
1809
|
-
clientWs.send(data);
|
|
1809
|
+
clientWs.send(data, { binary: true, compress: false });
|
|
1810
1810
|
}
|
|
1811
1811
|
}
|
|
1812
1812
|
return;
|
|
@@ -1824,6 +1824,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
|
|
|
1824
1824
|
const { proxyId, name } = result.message;
|
|
1825
1825
|
const status = registry.registerProxy(proxyId, proxyWs, name);
|
|
1826
1826
|
proxyWs.proxyId = proxyId;
|
|
1827
|
+
if (status === "reconnected") ptySnapshotRoutes.clearProxy(proxyId);
|
|
1827
1828
|
logger.info({ proxyId, status }, "Proxy registered");
|
|
1828
1829
|
proxyWs.send(
|
|
1829
1830
|
serializeControl({
|
|
@@ -1839,6 +1840,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
|
|
|
1839
1840
|
}
|
|
1840
1841
|
if (result.kind === "control" && result.message.type === "proxy_disconnect") {
|
|
1841
1842
|
if (proxyWs.proxyId) {
|
|
1843
|
+
ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
|
|
1842
1844
|
notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
|
|
1843
1845
|
registry.unregisterProxy(proxyWs.proxyId);
|
|
1844
1846
|
logger.info(
|
|
@@ -1895,6 +1897,35 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
|
|
|
1895
1897
|
remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
|
|
1896
1898
|
return;
|
|
1897
1899
|
}
|
|
1900
|
+
if (proxyWs.proxyId && result.message.type === "session_snapshot") {
|
|
1901
|
+
const route = ptySnapshotRoutes.resolve(
|
|
1902
|
+
proxyWs.proxyId,
|
|
1903
|
+
result.message.sessionId,
|
|
1904
|
+
result.message.requestId,
|
|
1905
|
+
proxyWs
|
|
1906
|
+
);
|
|
1907
|
+
if (route.kind === "matched" && route.clientWs.readyState === WebSocket4.OPEN) {
|
|
1908
|
+
if (chaos) {
|
|
1909
|
+
chaos.send(route.clientWs, raw, {
|
|
1910
|
+
direction: "proxy_to_client",
|
|
1911
|
+
type: result.message.type
|
|
1912
|
+
});
|
|
1913
|
+
} else {
|
|
1914
|
+
route.clientWs.send(raw);
|
|
1915
|
+
}
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
logger.debug(
|
|
1919
|
+
{
|
|
1920
|
+
proxyId: proxyWs.proxyId,
|
|
1921
|
+
sessionId: result.message.sessionId,
|
|
1922
|
+
requestId: result.message.requestId,
|
|
1923
|
+
route: route.kind
|
|
1924
|
+
},
|
|
1925
|
+
"Unmatched PTY snapshot dropped"
|
|
1926
|
+
);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1898
1929
|
if (isProxyToClientRelayControlType(result.message.type)) {
|
|
1899
1930
|
if (!proxyWs.proxyId) {
|
|
1900
1931
|
rejectNotRegistered(proxyWs);
|
|
@@ -1953,6 +1984,7 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
|
|
|
1953
1984
|
);
|
|
1954
1985
|
return;
|
|
1955
1986
|
}
|
|
1987
|
+
ptySnapshotRoutes.clearProxy(proxyWs.proxyId);
|
|
1956
1988
|
notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
|
|
1957
1989
|
try {
|
|
1958
1990
|
registry.transitionProxy(proxyWs.proxyId, "online", "offline");
|
|
@@ -2873,7 +2905,7 @@ function sendRemoteFileUploadUrlFailure(ws, requestId, sessionId, error, errorCo
|
|
|
2873
2905
|
})
|
|
2874
2906
|
);
|
|
2875
2907
|
}
|
|
2876
|
-
function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
|
|
2908
|
+
function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
|
|
2877
2909
|
const clientWs = ws;
|
|
2878
2910
|
clientWs.isAlive = true;
|
|
2879
2911
|
registry.addClientWs(clientWs, connectionInfo);
|
|
@@ -3113,6 +3145,48 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
|
|
|
3113
3145
|
}
|
|
3114
3146
|
const proxyWs = registry.getProxy(targetProxyId);
|
|
3115
3147
|
if (proxyWs && proxyWs.readyState === WebSocket7.OPEN) {
|
|
3148
|
+
if (msg.type === "session_subscribe") {
|
|
3149
|
+
const registration = ptySnapshotRoutes.register(
|
|
3150
|
+
targetProxyId,
|
|
3151
|
+
msg.sessionId,
|
|
3152
|
+
msg.requestId,
|
|
3153
|
+
clientWs,
|
|
3154
|
+
proxyWs
|
|
3155
|
+
);
|
|
3156
|
+
if (registration === "duplicate") {
|
|
3157
|
+
logger.debug(
|
|
3158
|
+
{ proxyId: targetProxyId, sessionId: msg.sessionId, requestId: msg.requestId },
|
|
3159
|
+
"Duplicate PTY snapshot subscribe suppressed"
|
|
3160
|
+
);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
if (registration !== "registered" && registration !== "retry_due") {
|
|
3164
|
+
logger.warn(
|
|
3165
|
+
{
|
|
3166
|
+
proxyId: targetProxyId,
|
|
3167
|
+
sessionId: msg.sessionId,
|
|
3168
|
+
requestId: msg.requestId,
|
|
3169
|
+
registration
|
|
3170
|
+
},
|
|
3171
|
+
"PTY snapshot subscribe route rejected"
|
|
3172
|
+
);
|
|
3173
|
+
clientWs.send(
|
|
3174
|
+
JSON.stringify({
|
|
3175
|
+
type: "relay_error",
|
|
3176
|
+
requestId: msg.requestId,
|
|
3177
|
+
code: RelayErrorCode.INVALID_MESSAGE,
|
|
3178
|
+
message: registration === "collision" ? "PTY snapshot requestId is already in use" : "Too many pending PTY snapshot requests"
|
|
3179
|
+
})
|
|
3180
|
+
);
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
if (registration === "retry_due") {
|
|
3184
|
+
logger.debug(
|
|
3185
|
+
{ proxyId: targetProxyId, sessionId: msg.sessionId, requestId: msg.requestId },
|
|
3186
|
+
"Retrying unanswered PTY snapshot subscribe"
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3116
3190
|
if (chaos) chaos.send(proxyWs, raw, { direction: "client_to_proxy", type: msg.type });
|
|
3117
3191
|
else proxyWs.send(raw);
|
|
3118
3192
|
} else {
|
|
@@ -3189,6 +3263,7 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
|
|
|
3189
3263
|
}
|
|
3190
3264
|
});
|
|
3191
3265
|
clientWs.on("close", (code, reason) => {
|
|
3266
|
+
ptySnapshotRoutes.abandonSocket(clientWs);
|
|
3192
3267
|
registry.removeClientWs(clientWs);
|
|
3193
3268
|
if (clientWs.clientId) {
|
|
3194
3269
|
registry.unbindClientSocket(clientWs.clientId, clientWs);
|
|
@@ -4095,7 +4170,7 @@ var RemoteFileBridge = class {
|
|
|
4095
4170
|
req.pause();
|
|
4096
4171
|
const frame = encodeFileStreamFrame(uploadId, pending.chunkSeq, Buffer.from(chunk));
|
|
4097
4172
|
pending.chunkSeq += 1;
|
|
4098
|
-
proxyWs.send(frame, { binary: true }, (err) => {
|
|
4173
|
+
proxyWs.send(frame, { binary: true, compress: false }, (err) => {
|
|
4099
4174
|
if (err) {
|
|
4100
4175
|
this.failUpload(uploadId, 502, err.message);
|
|
4101
4176
|
return;
|
|
@@ -4411,10 +4486,138 @@ function mountWebApp(app, options) {
|
|
|
4411
4486
|
return true;
|
|
4412
4487
|
}
|
|
4413
4488
|
|
|
4489
|
+
// src/pty-snapshot-route-registry.ts
|
|
4490
|
+
var DEFAULT_MAX_ENTRIES = 4096;
|
|
4491
|
+
var DEFAULT_MAX_PENDING_PER_CLIENT = 64;
|
|
4492
|
+
var DEFAULT_RETRY_FORWARD_INTERVAL_MS = 25e3;
|
|
4493
|
+
var DEFAULT_PENDING_TTL_MS = 5 * 6e4;
|
|
4494
|
+
var DEFAULT_TOMBSTONE_TTL_MS = 12e4;
|
|
4495
|
+
function routeKey(proxyId, sessionId, requestId) {
|
|
4496
|
+
return JSON.stringify([proxyId, sessionId, requestId]);
|
|
4497
|
+
}
|
|
4498
|
+
var PtySnapshotRouteRegistry = class {
|
|
4499
|
+
routes = /* @__PURE__ */ new Map();
|
|
4500
|
+
maxEntries;
|
|
4501
|
+
maxPendingPerClient;
|
|
4502
|
+
retryForwardIntervalMs;
|
|
4503
|
+
pendingTtlMs;
|
|
4504
|
+
tombstoneTtlMs;
|
|
4505
|
+
now;
|
|
4506
|
+
constructor(options = {}) {
|
|
4507
|
+
this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
4508
|
+
this.maxPendingPerClient = Math.max(
|
|
4509
|
+
1,
|
|
4510
|
+
options.maxPendingPerClient ?? DEFAULT_MAX_PENDING_PER_CLIENT
|
|
4511
|
+
);
|
|
4512
|
+
this.retryForwardIntervalMs = Math.max(
|
|
4513
|
+
1,
|
|
4514
|
+
options.retryForwardIntervalMs ?? DEFAULT_RETRY_FORWARD_INTERVAL_MS
|
|
4515
|
+
);
|
|
4516
|
+
this.pendingTtlMs = Math.max(1, options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS);
|
|
4517
|
+
this.tombstoneTtlMs = Math.max(1, options.tombstoneTtlMs ?? DEFAULT_TOMBSTONE_TTL_MS);
|
|
4518
|
+
this.now = options.now ?? Date.now;
|
|
4519
|
+
}
|
|
4520
|
+
register(proxyId, sessionId, requestId, clientWs, proxyWs) {
|
|
4521
|
+
const now = this.now();
|
|
4522
|
+
this.pruneExpired(now);
|
|
4523
|
+
const key = routeKey(proxyId, sessionId, requestId);
|
|
4524
|
+
const existing = this.routes.get(key);
|
|
4525
|
+
if (existing) {
|
|
4526
|
+
if (existing.state === "pending" && existing.clientWs === clientWs && existing.proxyWs === proxyWs) {
|
|
4527
|
+
if (now - existing.lastForwardedAt >= this.retryForwardIntervalMs) {
|
|
4528
|
+
existing.lastForwardedAt = now;
|
|
4529
|
+
return "retry_due";
|
|
4530
|
+
}
|
|
4531
|
+
return "duplicate";
|
|
4532
|
+
}
|
|
4533
|
+
return "collision";
|
|
4534
|
+
}
|
|
4535
|
+
let clientPendingCount = 0;
|
|
4536
|
+
for (const route of this.routes.values()) {
|
|
4537
|
+
if (route.state === "pending" && route.clientWs === clientWs) {
|
|
4538
|
+
clientPendingCount += 1;
|
|
4539
|
+
if (clientPendingCount >= this.maxPendingPerClient) {
|
|
4540
|
+
return "client_capacity_exceeded";
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
if (this.routes.size >= this.maxEntries) {
|
|
4545
|
+
let oldestTombstoneKey;
|
|
4546
|
+
let oldestTombstoneExpiry = Number.POSITIVE_INFINITY;
|
|
4547
|
+
for (const [candidateKey, route] of this.routes) {
|
|
4548
|
+
if (route.state === "tombstone" && route.expiresAt < oldestTombstoneExpiry) {
|
|
4549
|
+
oldestTombstoneKey = candidateKey;
|
|
4550
|
+
oldestTombstoneExpiry = route.expiresAt;
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
if (oldestTombstoneKey) this.routes.delete(oldestTombstoneKey);
|
|
4554
|
+
else return "capacity_exceeded";
|
|
4555
|
+
}
|
|
4556
|
+
this.routes.set(key, {
|
|
4557
|
+
state: "pending",
|
|
4558
|
+
proxyId,
|
|
4559
|
+
proxyWs,
|
|
4560
|
+
clientWs,
|
|
4561
|
+
lastForwardedAt: now,
|
|
4562
|
+
expiresAt: now + this.pendingTtlMs
|
|
4563
|
+
});
|
|
4564
|
+
return "registered";
|
|
4565
|
+
}
|
|
4566
|
+
resolve(proxyId, sessionId, requestId, proxyWs) {
|
|
4567
|
+
const now = this.now();
|
|
4568
|
+
this.pruneExpired(now);
|
|
4569
|
+
const key = routeKey(proxyId, sessionId, requestId);
|
|
4570
|
+
const route = this.routes.get(key);
|
|
4571
|
+
if (!route) return { kind: "unmatched" };
|
|
4572
|
+
if (route.state === "tombstone") return { kind: "tombstone" };
|
|
4573
|
+
if (route.proxyWs !== proxyWs) return { kind: "stale_proxy" };
|
|
4574
|
+
this.routes.set(key, {
|
|
4575
|
+
state: "tombstone",
|
|
4576
|
+
proxyId,
|
|
4577
|
+
expiresAt: now + this.tombstoneTtlMs
|
|
4578
|
+
});
|
|
4579
|
+
return { kind: "matched", clientWs: route.clientWs };
|
|
4580
|
+
}
|
|
4581
|
+
abandonSocket(clientWs) {
|
|
4582
|
+
const now = this.now();
|
|
4583
|
+
this.pruneExpired(now);
|
|
4584
|
+
for (const [key, route] of this.routes) {
|
|
4585
|
+
if (route.state === "pending" && route.clientWs === clientWs) {
|
|
4586
|
+
this.routes.set(key, {
|
|
4587
|
+
state: "tombstone",
|
|
4588
|
+
proxyId: route.proxyId,
|
|
4589
|
+
expiresAt: now + this.tombstoneTtlMs
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
clearProxy(proxyId) {
|
|
4595
|
+
for (const [key, route] of this.routes) {
|
|
4596
|
+
if (route.proxyId === proxyId) this.routes.delete(key);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
pruneExpired(now) {
|
|
4600
|
+
for (const [key, route] of this.routes) {
|
|
4601
|
+
if (route.expiresAt <= now) this.routes.delete(key);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
};
|
|
4605
|
+
|
|
4414
4606
|
// src/server.ts
|
|
4415
4607
|
var MODULE_DIR = dirname3(fileURLToPath2(import.meta.url));
|
|
4416
4608
|
var PACKAGED_FONTS_DIR = resolve(MODULE_DIR, "../assets/fonts");
|
|
4417
4609
|
var PACKAGED_WEB_DIR = resolve(MODULE_DIR, "../assets/web");
|
|
4610
|
+
var DATA_CHANNEL_MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
4611
|
+
var DATA_CHANNEL_COMPRESSION = {
|
|
4612
|
+
clientNoContextTakeover: true,
|
|
4613
|
+
serverNoContextTakeover: true,
|
|
4614
|
+
threshold: 32 * 1024,
|
|
4615
|
+
concurrencyLimit: 4,
|
|
4616
|
+
zlibDeflateOptions: {
|
|
4617
|
+
level: 3,
|
|
4618
|
+
memLevel: 7
|
|
4619
|
+
}
|
|
4620
|
+
};
|
|
4418
4621
|
function firstHeaderValue(value) {
|
|
4419
4622
|
if (Array.isArray(value)) return value[0];
|
|
4420
4623
|
return value;
|
|
@@ -4444,6 +4647,7 @@ function createRelayServer(options) {
|
|
|
4444
4647
|
);
|
|
4445
4648
|
}
|
|
4446
4649
|
const registry = new RelayRegistry();
|
|
4650
|
+
const ptySnapshotRoutes = new PtySnapshotRouteRegistry();
|
|
4447
4651
|
const remoteFileBridge = new RemoteFileBridge({ registry, logger });
|
|
4448
4652
|
const voiceConfigStore = createVoiceConfigStore({
|
|
4449
4653
|
dataDir,
|
|
@@ -4513,8 +4717,16 @@ function createRelayServer(options) {
|
|
|
4513
4717
|
mountWebApp(app, { webAssetDir, logger });
|
|
4514
4718
|
}
|
|
4515
4719
|
const httpServer = createServer(app);
|
|
4516
|
-
const proxyWss = new WebSocketServer({
|
|
4517
|
-
|
|
4720
|
+
const proxyWss = new WebSocketServer({
|
|
4721
|
+
noServer: true,
|
|
4722
|
+
maxPayload: DATA_CHANNEL_MAX_PAYLOAD_BYTES,
|
|
4723
|
+
perMessageDeflate: DATA_CHANNEL_COMPRESSION
|
|
4724
|
+
});
|
|
4725
|
+
const clientWss = new WebSocketServer({
|
|
4726
|
+
noServer: true,
|
|
4727
|
+
maxPayload: DATA_CHANNEL_MAX_PAYLOAD_BYTES,
|
|
4728
|
+
perMessageDeflate: DATA_CHANNEL_COMPRESSION
|
|
4729
|
+
});
|
|
4518
4730
|
const voiceAsrWss = new WebSocketServer({ noServer: true });
|
|
4519
4731
|
const voiceTtsWss = new WebSocketServer({ noServer: true });
|
|
4520
4732
|
httpServer.on("upgrade", (request, socket, head) => {
|
|
@@ -4579,13 +4791,14 @@ function createRelayServer(options) {
|
|
|
4579
4791
|
socket.destroy();
|
|
4580
4792
|
});
|
|
4581
4793
|
proxyWss.on("connection", (ws) => {
|
|
4582
|
-
handleProxyConnection(ws, registry, logger, relayChaos, remoteFileBridge);
|
|
4794
|
+
handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, relayChaos, remoteFileBridge);
|
|
4583
4795
|
});
|
|
4584
4796
|
clientWss.on("connection", (ws, request) => {
|
|
4585
4797
|
handleClientConnection(
|
|
4586
4798
|
ws,
|
|
4587
4799
|
registry,
|
|
4588
4800
|
logger,
|
|
4801
|
+
ptySnapshotRoutes,
|
|
4589
4802
|
relayChaos,
|
|
4590
4803
|
voiceConfigStore,
|
|
4591
4804
|
voiceProviders,
|
|
@@ -4681,4 +4894,4 @@ export {
|
|
|
4681
4894
|
parseRelayChaosFromEnv,
|
|
4682
4895
|
createRelayServer
|
|
4683
4896
|
};
|
|
4684
|
-
//# sourceMappingURL=chunk-
|
|
4897
|
+
//# sourceMappingURL=chunk-4Q4EAZP6.js.map
|