botschat 0.1.14 → 0.1.15
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/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +95 -41
- package/packages/api/src/index.ts +42 -13
- package/packages/plugin/dist/src/ws-client.d.ts +1 -0
- package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
- package/packages/plugin/dist/src/ws-client.js +41 -4
- package/packages/plugin/dist/src/ws-client.js.map +1 -1
- package/packages/plugin/package.json +1 -1
- package/packages/web/dist/assets/index-CbCpFrA9.js +2 -0
- package/packages/web/dist/assets/index-Ct0m11C8.js +2 -0
- package/packages/web/dist/assets/{index-BJye3VHV.js → index-CvbTpaza.js} +122 -122
- package/packages/web/dist/assets/{index-CkIgZfHf.js → index-DsWBWQD6.js} +1 -1
- package/packages/web/dist/assets/index-GwprVhDP.js +1 -0
- package/packages/web/dist/assets/{index-CNSCbd7_.css → index-cm_3YFsA.css} +1 -1
- package/packages/web/dist/assets/{index-CQPXprFz.js → index-dMn_npR3.js} +1 -1
- package/packages/web/dist/assets/{index.esm-DgcFARs7.js → index.esm-DdTIpXjl.js} +1 -1
- package/packages/web/dist/assets/{web-CnOlwlZw.js → web-DIeOUVhn.js} +1 -1
- package/packages/web/dist/assets/{web-Bfku9Io_.js → web-Dft_LGIH.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/components/ChatWindow.tsx +7 -2
- package/packages/web/src/components/ThreadPanel.tsx +13 -4
- package/packages/web/src/utils/time.ts +23 -0
- package/packages/web/dist/assets/index-CPOiRHa4.js +0 -2
- package/packages/web/dist/assets/index-DbUyNI4d.js +0 -1
- package/packages/web/dist/assets/index-Dpvhc_dU.js +0 -2
package/package.json
CHANGED
|
@@ -32,6 +32,9 @@ export class ConnectionDO implements DurableObject {
|
|
|
32
32
|
/** Browser sessions that report themselves in foreground (push notifications are suppressed). */
|
|
33
33
|
private foregroundSessions = new Set<string>();
|
|
34
34
|
|
|
35
|
+
/** Timestamp of last accepted OpenClaw WebSocket (in-memory, no storage write). */
|
|
36
|
+
private lastOpenClawAcceptedAt = 0;
|
|
37
|
+
|
|
35
38
|
constructor(state: DurableObjectState, env: Env) {
|
|
36
39
|
this.state = state;
|
|
37
40
|
this.env = env;
|
|
@@ -39,16 +42,33 @@ export class ConnectionDO implements DurableObject {
|
|
|
39
42
|
|
|
40
43
|
/** Handle incoming HTTP requests (WebSocket upgrades). */
|
|
41
44
|
async fetch(request: Request): Promise<Response> {
|
|
45
|
+
try {
|
|
46
|
+
return await this._fetch(request);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const msg = String(err);
|
|
49
|
+
if (msg.includes("Exceeded")) {
|
|
50
|
+
console.error("[DO] Storage limit exceeded:", msg);
|
|
51
|
+
return new Response("Storage limit exceeded, retry later", {
|
|
52
|
+
status: 503,
|
|
53
|
+
headers: { "Retry-After": "300" },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async _fetch(request: Request): Promise<Response> {
|
|
42
61
|
const url = new URL(request.url);
|
|
43
62
|
|
|
44
63
|
// Route: /gateway/:accountId — OpenClaw plugin connects here
|
|
45
64
|
if (url.pathname.startsWith("/gateway/")) {
|
|
46
|
-
// Extract and store userId from the gateway path
|
|
47
65
|
const userId = url.pathname.split("/gateway/")[1]?.split("?")[0];
|
|
48
66
|
if (userId) {
|
|
49
|
-
await this.state.storage.
|
|
67
|
+
const stored = await this.state.storage.get<string>("userId");
|
|
68
|
+
if (stored !== userId) {
|
|
69
|
+
await this.state.storage.put("userId", userId);
|
|
70
|
+
}
|
|
50
71
|
}
|
|
51
|
-
// Check if the API worker already verified the token against D1
|
|
52
72
|
const preVerified = url.searchParams.get("verified") === "1";
|
|
53
73
|
return this.handleOpenClawConnect(request, preVerified);
|
|
54
74
|
}
|
|
@@ -92,22 +112,32 @@ export class ConnectionDO implements DurableObject {
|
|
|
92
112
|
|
|
93
113
|
/** Called when a WebSocket receives a message (wakes from hibernation). */
|
|
94
114
|
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
|
|
95
|
-
const tag = this.getTag(ws);
|
|
96
|
-
const data = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
97
|
-
|
|
98
|
-
let parsed: Record<string, unknown>;
|
|
99
115
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return; // Ignore malformed JSON
|
|
103
|
-
}
|
|
116
|
+
const tag = this.getTag(ws);
|
|
117
|
+
const data = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
104
118
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
let parsed: Record<string, unknown>;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(data);
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (tag === "openclaw") {
|
|
127
|
+
await this.handleOpenClawMessage(ws, parsed);
|
|
128
|
+
} else if (tag?.startsWith("browser:")) {
|
|
129
|
+
await this.handleBrowserMessage(ws, parsed);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const msg = String(err);
|
|
133
|
+
if (msg.includes("Exceeded")) {
|
|
134
|
+
console.error("[DO] Storage limit exceeded in webSocketMessage:", msg);
|
|
135
|
+
try {
|
|
136
|
+
ws.send(JSON.stringify({ type: "error", message: "Storage limit exceeded, retry later" }));
|
|
137
|
+
} catch { /* socket may already be dead */ }
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw err;
|
|
111
141
|
}
|
|
112
142
|
}
|
|
113
143
|
|
|
@@ -139,10 +169,31 @@ export class ConnectionDO implements DurableObject {
|
|
|
139
169
|
return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
140
170
|
}
|
|
141
171
|
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const cooldownMs = 30_000;
|
|
174
|
+
if (now - this.lastOpenClawAcceptedAt < cooldownMs) {
|
|
175
|
+
const retryAfter = Math.ceil((cooldownMs - (now - this.lastOpenClawAcceptedAt)) / 1000);
|
|
176
|
+
return new Response("Too many connections, retry later", {
|
|
177
|
+
status: 429,
|
|
178
|
+
headers: { "Retry-After": String(retryAfter) },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
this.lastOpenClawAcceptedAt = now;
|
|
182
|
+
|
|
183
|
+
// Safety valve: if stale openclaw sockets accumulated (e.g. from
|
|
184
|
+
// rapid reconnects that authenticated but then lost their edge
|
|
185
|
+
// connection), close them all before accepting a new one.
|
|
186
|
+
const existing = this.state.getWebSockets("openclaw");
|
|
187
|
+
if (existing.length > 3) {
|
|
188
|
+
console.warn(`[DO] Safety valve: ${existing.length} openclaw sockets, closing all`);
|
|
189
|
+
for (const s of existing) {
|
|
190
|
+
try { s.close(4009, "replaced"); } catch { /* dead */ }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
142
194
|
const pair = new WebSocketPair();
|
|
143
195
|
const [client, server] = [pair[0], pair[1]];
|
|
144
196
|
|
|
145
|
-
// Accept with Hibernation API, tag as "openclaw"
|
|
146
197
|
this.state.acceptWebSocket(server, ["openclaw"]);
|
|
147
198
|
|
|
148
199
|
// If the API worker already verified the token against D1, mark as
|
|
@@ -187,31 +238,26 @@ export class ConnectionDO implements DurableObject {
|
|
|
187
238
|
const isValid = attachment?.preVerified || await this.validatePairingToken(token);
|
|
188
239
|
|
|
189
240
|
if (isValid) {
|
|
190
|
-
// Close
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// the old socket's death yet (no close frame → no webSocketClose
|
|
195
|
-
// callback yet). Without this cleanup, getOpenClawSocket() could
|
|
196
|
-
// return a stale/dead socket, silently dropping all messages.
|
|
241
|
+
// Close ALL other openclaw sockets. Use custom code 4009 so
|
|
242
|
+
// well-behaved plugins know they were replaced (not a crash)
|
|
243
|
+
// and should NOT reconnect. The Worker-level rate limit (10s)
|
|
244
|
+
// prevents the resulting close event from flooding the DO.
|
|
197
245
|
const existingSockets = this.state.getWebSockets("openclaw");
|
|
246
|
+
let closedCount = 0;
|
|
198
247
|
for (const oldWs of existingSockets) {
|
|
199
248
|
if (oldWs !== ws) {
|
|
200
249
|
try {
|
|
201
|
-
oldWs.close(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
250
|
+
oldWs.close(4009, "replaced");
|
|
251
|
+
closedCount++;
|
|
252
|
+
} catch { /* already dead */ }
|
|
205
253
|
}
|
|
206
254
|
}
|
|
207
255
|
|
|
208
256
|
ws.serializeAttachment({ ...attachment, authenticated: true });
|
|
209
|
-
// Include userId so the plugin can derive the E2E key
|
|
210
257
|
const userId = await this.state.storage.get<string>("userId");
|
|
211
|
-
console.log(`[DO] auth.ok → userId=${userId},
|
|
258
|
+
console.log(`[DO] auth.ok → userId=${userId}, closed=${closedCount}, total=${existingSockets.length}`);
|
|
212
259
|
ws.send(JSON.stringify({ type: "auth.ok", userId }));
|
|
213
|
-
|
|
214
|
-
if (msg.model) {
|
|
260
|
+
if (msg.model && msg.model !== this.defaultModel) {
|
|
215
261
|
this.defaultModel = msg.model as string;
|
|
216
262
|
await this.state.storage.put("defaultModel", this.defaultModel);
|
|
217
263
|
}
|
|
@@ -295,20 +341,24 @@ export class ConnectionDO implements DurableObject {
|
|
|
295
341
|
await this.handleTaskScanResult(msg);
|
|
296
342
|
}
|
|
297
343
|
|
|
298
|
-
// Handle models list from plugin — persist to storage and broadcast to browsers
|
|
299
344
|
if (msg.type === "models.list") {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
345
|
+
const newModels = (msg.models as Array<{ id: string; name: string; provider: string }>) ?? [];
|
|
346
|
+
const changed = JSON.stringify(newModels) !== JSON.stringify(this.cachedModels);
|
|
347
|
+
this.cachedModels = newModels;
|
|
348
|
+
if (changed) {
|
|
349
|
+
await this.state.storage.put("cachedModels", this.cachedModels);
|
|
350
|
+
console.log(`[DO] Persisted ${this.cachedModels.length} models to storage`);
|
|
351
|
+
}
|
|
303
352
|
this.broadcastToBrowsers(
|
|
304
353
|
JSON.stringify({ type: "connection.status", openclawConnected: true, defaultModel: this.defaultModel, models: this.cachedModels }),
|
|
305
354
|
);
|
|
306
355
|
}
|
|
307
356
|
|
|
308
|
-
// Plugin applied BotsChat default model to OpenClaw config — update and broadcast
|
|
309
357
|
if (msg.type === "defaultModel.updated" && typeof msg.model === "string") {
|
|
310
|
-
this.defaultModel
|
|
311
|
-
|
|
358
|
+
if (msg.model !== this.defaultModel) {
|
|
359
|
+
this.defaultModel = msg.model;
|
|
360
|
+
await this.state.storage.put("defaultModel", this.defaultModel);
|
|
361
|
+
}
|
|
312
362
|
this.broadcastToBrowsers(
|
|
313
363
|
JSON.stringify({ type: "connection.status", openclawConnected: true, defaultModel: this.defaultModel, models: this.cachedModels }),
|
|
314
364
|
);
|
|
@@ -1299,7 +1349,11 @@ export class ConnectionDO implements DurableObject {
|
|
|
1299
1349
|
.first<{ user_id: string }>();
|
|
1300
1350
|
|
|
1301
1351
|
const isValid = !!row;
|
|
1302
|
-
|
|
1352
|
+
try {
|
|
1353
|
+
await this.state.storage.put(cacheKey, { valid: isValid, cachedAt: Date.now() });
|
|
1354
|
+
} catch {
|
|
1355
|
+
// Non-critical — skip caching if storage is full
|
|
1356
|
+
}
|
|
1303
1357
|
return isValid;
|
|
1304
1358
|
} catch (err) {
|
|
1305
1359
|
console.error("[DO] Failed to validate pairing token against D1:", err);
|
|
@@ -365,7 +365,6 @@ async function verifyUserAccess(c: { req: { header: (n: string) => string | unde
|
|
|
365
365
|
app.all("/api/gateway/:connId", async (c) => {
|
|
366
366
|
let userId = c.req.param("connId");
|
|
367
367
|
|
|
368
|
-
// If connId is not a real user ID (e.g. "default"), resolve via token
|
|
369
368
|
if (!userId.startsWith("u_")) {
|
|
370
369
|
const token =
|
|
371
370
|
c.req.query("token") ??
|
|
@@ -376,7 +375,6 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
376
375
|
return c.json({ error: "Token required for gateway connection" }, 401);
|
|
377
376
|
}
|
|
378
377
|
|
|
379
|
-
// Look up user by pairing token (exclude revoked tokens)
|
|
380
378
|
const row = await c.env.DB.prepare(
|
|
381
379
|
"SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
|
|
382
380
|
)
|
|
@@ -387,26 +385,57 @@ app.all("/api/gateway/:connId", async (c) => {
|
|
|
387
385
|
return c.json({ error: "Invalid pairing token" }, 401);
|
|
388
386
|
}
|
|
389
387
|
userId = row.user_id;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- Worker-level rate limit (Cache API) ---
|
|
391
|
+
// Protects the DO from being woken up during reconnection storms.
|
|
392
|
+
// The Cache API persists across Worker isolates within the same colo.
|
|
393
|
+
const GATEWAY_COOLDOWN_S = 10;
|
|
394
|
+
const cache = caches.default;
|
|
395
|
+
const rateCacheUrl = `https://rate.internal/gateway/${userId}`;
|
|
396
|
+
const rateCacheReq = new Request(rateCacheUrl);
|
|
397
|
+
const rateCached = await cache.match(rateCacheReq);
|
|
398
|
+
if (rateCached) {
|
|
399
|
+
return c.text("Too many connections, retry later", 429, {
|
|
400
|
+
"Retry-After": String(GATEWAY_COOLDOWN_S),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
390
403
|
|
|
391
|
-
|
|
404
|
+
// Audit: update pairing token stats (only when not rate-limited)
|
|
405
|
+
const token = c.req.query("token") ?? c.req.header("X-Pairing-Token");
|
|
406
|
+
if (token) {
|
|
392
407
|
const clientIp = c.req.header("CF-Connecting-IP") ?? c.req.header("X-Forwarded-For") ?? "unknown";
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
.bind(clientIp, token)
|
|
399
|
-
|
|
408
|
+
c.executionCtx.waitUntil(
|
|
409
|
+
c.env.DB.prepare(
|
|
410
|
+
`UPDATE pairing_tokens
|
|
411
|
+
SET last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1
|
|
412
|
+
WHERE token = ?`,
|
|
413
|
+
).bind(clientIp, token).run(),
|
|
414
|
+
);
|
|
400
415
|
}
|
|
401
416
|
|
|
402
417
|
const doId = c.env.CONNECTION_DO.idFromName(userId);
|
|
403
418
|
const stub = c.env.CONNECTION_DO.get(doId);
|
|
404
419
|
const url = new URL(c.req.url);
|
|
405
|
-
// Pass verified userId to DO — the API worker already validated the token
|
|
406
|
-
// against D1 above, so DO can trust this.
|
|
407
420
|
url.pathname = `/gateway/${userId}`;
|
|
408
421
|
url.searchParams.set("verified", "1");
|
|
409
|
-
|
|
422
|
+
const doResp = await stub.fetch(new Request(url.toString(), c.req.raw));
|
|
423
|
+
|
|
424
|
+
// Cache the rate limit after the DO responds (success or rate-limited).
|
|
425
|
+
// 101 = WebSocket accepted; 429 = DO's own rate limit.
|
|
426
|
+
// Either way, prevent further DO wake-ups for GATEWAY_COOLDOWN_S.
|
|
427
|
+
if (doResp.status === 101 || doResp.status === 429) {
|
|
428
|
+
c.executionCtx.waitUntil(
|
|
429
|
+
cache.put(
|
|
430
|
+
rateCacheReq,
|
|
431
|
+
new Response(null, {
|
|
432
|
+
headers: { "Cache-Control": `public, max-age=${GATEWAY_COOLDOWN_S}` },
|
|
433
|
+
}),
|
|
434
|
+
),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return doResp;
|
|
410
439
|
});
|
|
411
440
|
|
|
412
441
|
// Browser client connects to: /api/ws/:userId/:sessionId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-client.d.ts","sourceRoot":"","sources":["../../src/ws-client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACpC,SAAS,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAC;IACvC,cAAc,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,GAAG,CAAC,EAAE;QACJ,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;KAC9B,CAAC;CACH,CAAC;
|
|
1
|
+
{"version":3,"file":"ws-client.d.ts","sourceRoot":"","sources":["../../src/ws-client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACpC,SAAS,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAC;IACvC,cAAc,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,GAAG,CAAC,EAAE;QACJ,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;KAC9B,CAAC;CACH,CAAC;AAOF;;;;;;;;;GASG;AACH,qBAAa,mBAAmB;IAWlB,OAAO,CAAC,IAAI;IAVxB,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,iBAAiB,CAA8C;IACvE,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,UAAU,CAAS;IACpB,MAAM,EAAE,UAAU,GAAG,IAAI,CAAQ;IACjC,aAAa,UAAS;gBAET,IAAI,EAAE,0BAA0B;IAEpD,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,6CAA6C;IAC7C,OAAO,IAAI,IAAI;IAgFf,4CAA4C;IAC5C,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAQ9B,6BAA6B;IAC7B,UAAU,IAAI,IAAI;YAoBJ,aAAa;IAyC3B,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,GAAG;CAMZ"}
|
|
@@ -2,6 +2,8 @@ import WebSocket from "ws";
|
|
|
2
2
|
import { deriveKey } from "./e2e-crypto.js";
|
|
3
3
|
const MIN_BACKOFF_MS = 1_000;
|
|
4
4
|
const MAX_BACKOFF_MS = 30_000;
|
|
5
|
+
/** Custom close code: server replaced this connection with a newer one. */
|
|
6
|
+
const CLOSE_REPLACED = 4009;
|
|
5
7
|
/**
|
|
6
8
|
* Manages a persistent outbound WebSocket connection from the OpenClaw
|
|
7
9
|
* plugin to the BotsChat cloud (ConnectionDO on Cloudflare).
|
|
@@ -17,6 +19,7 @@ export class BotsChatCloudClient {
|
|
|
17
19
|
ws = null;
|
|
18
20
|
reconnectTimer = null;
|
|
19
21
|
pingTimer = null;
|
|
22
|
+
backoffResetTimer = null;
|
|
20
23
|
backoffMs = MIN_BACKOFF_MS;
|
|
21
24
|
intentionalClose = false;
|
|
22
25
|
_connected = false;
|
|
@@ -70,13 +73,36 @@ export class BotsChatCloudClient {
|
|
|
70
73
|
this.log("warn", `WebSocket closed: code=${code} reason=${reason?.toString() ?? ""}`);
|
|
71
74
|
this.setConnected(false);
|
|
72
75
|
this.stopPing();
|
|
76
|
+
if (this.backoffResetTimer) {
|
|
77
|
+
clearTimeout(this.backoffResetTimer);
|
|
78
|
+
this.backoffResetTimer = null;
|
|
79
|
+
}
|
|
80
|
+
if (code === CLOSE_REPLACED) {
|
|
81
|
+
this.log("info", "Connection replaced by server — not reconnecting");
|
|
82
|
+
this.intentionalClose = true;
|
|
83
|
+
}
|
|
73
84
|
if (!this.intentionalClose) {
|
|
74
85
|
this.scheduleReconnect();
|
|
75
86
|
}
|
|
76
87
|
});
|
|
88
|
+
// Detect HTTP-level rejections before the WebSocket is established.
|
|
89
|
+
// Node ws emits 'unexpected-response' when the server returns non-101.
|
|
90
|
+
this.ws.on("unexpected-response", (_req, res) => {
|
|
91
|
+
const status = res?.statusCode ?? 0;
|
|
92
|
+
const retryAfter = parseInt(res?.headers?.["retry-after"] ?? "0", 10);
|
|
93
|
+
if (status === 429 && retryAfter > 0) {
|
|
94
|
+
this.log("warn", `Rate-limited (429), backing off ${retryAfter}s`);
|
|
95
|
+
this.backoffMs = retryAfter * 1000;
|
|
96
|
+
}
|
|
97
|
+
else if (status === 503) {
|
|
98
|
+
const secs = retryAfter || 300;
|
|
99
|
+
this.log("warn", `Service unavailable (503), backing off ${secs}s`);
|
|
100
|
+
this.backoffMs = secs * 1000;
|
|
101
|
+
}
|
|
102
|
+
// ws will emit 'close' after this, triggering scheduleReconnect
|
|
103
|
+
});
|
|
77
104
|
this.ws.on("error", (err) => {
|
|
78
105
|
this.log("error", `WebSocket error: ${err.message}`);
|
|
79
|
-
// The "close" event will fire after this, triggering reconnect
|
|
80
106
|
});
|
|
81
107
|
}
|
|
82
108
|
/** Send a message to the BotsChat cloud. */
|
|
@@ -95,6 +121,10 @@ export class BotsChatCloudClient {
|
|
|
95
121
|
clearTimeout(this.reconnectTimer);
|
|
96
122
|
this.reconnectTimer = null;
|
|
97
123
|
}
|
|
124
|
+
if (this.backoffResetTimer) {
|
|
125
|
+
clearTimeout(this.backoffResetTimer);
|
|
126
|
+
this.backoffResetTimer = null;
|
|
127
|
+
}
|
|
98
128
|
if (this.ws) {
|
|
99
129
|
this.ws.close(1000, "shutdown");
|
|
100
130
|
this.ws = null;
|
|
@@ -106,9 +136,16 @@ export class BotsChatCloudClient {
|
|
|
106
136
|
switch (msg.type) {
|
|
107
137
|
case "auth.ok":
|
|
108
138
|
this.log("info", `Authenticated with BotsChat cloud (userId=${msg.userId}, hasE2ePwd=${!!this.opts.e2ePassword})`);
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
139
|
+
// Delay backoff reset: only reset to MIN after the connection has
|
|
140
|
+
// been stable for 10s. If the connection drops immediately (e.g.
|
|
141
|
+
// server-side replacement), we keep the current backoff to avoid
|
|
142
|
+
// a fast reconnect loop.
|
|
143
|
+
if (this.backoffResetTimer)
|
|
144
|
+
clearTimeout(this.backoffResetTimer);
|
|
145
|
+
this.backoffResetTimer = setTimeout(() => {
|
|
146
|
+
this.backoffMs = MIN_BACKOFF_MS;
|
|
147
|
+
this.backoffResetTimer = null;
|
|
148
|
+
}, 10_000);
|
|
112
149
|
this.setConnected(true);
|
|
113
150
|
this.startPing();
|
|
114
151
|
// Derive E2E key AFTER marking connected (PBKDF2 is slow ~1-2s).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../../src/ws-client.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAoB5C,MAAM,cAAc,GAAG,KAAK,CAAC;AAC7B,MAAM,cAAc,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../../src/ws-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/web-DIeOUVhn.js","assets/index-CvbTpaza.js","assets/index-cm_3YFsA.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{r as o,f as t}from"./index-CvbTpaza.js";const n=o("SocialLogin",{web:()=>t(()=>import("./web-DIeOUVhn.js"),__vite__mapDeps([0,1,2])).then(e=>new e.SocialLoginWeb)});export{n as SocialLogin};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/web-Dft_LGIH.js","assets/index-CvbTpaza.js","assets/index-cm_3YFsA.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{r as p,f as r}from"./index-CvbTpaza.js";const o=p("App",{web:()=>r(()=>import("./web-Dft_LGIH.js"),__vite__mapDeps([0,1,2])).then(e=>new e.AppWeb)});export{o as App};
|