@mininglamp-oss/cc-channel-octo 1.0.1-dev.0ac574a
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/CHANGELOG.md +361 -0
- package/LICENSE +191 -0
- package/README.md +577 -0
- package/config.bot.example.json +15 -0
- package/config.example.json +33 -0
- package/dist/agent-bridge.d.ts +91 -0
- package/dist/agent-bridge.js +397 -0
- package/dist/agent-bridge.js.map +1 -0
- package/dist/cli.d.ts +109 -0
- package/dist/cli.js +467 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +57 -0
- package/dist/commands.js +121 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +294 -0
- package/dist/config.js +344 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +11 -0
- package/dist/configure.js +106 -0
- package/dist/configure.js.map +1 -0
- package/dist/cron-evaluator.d.ts +53 -0
- package/dist/cron-evaluator.js +191 -0
- package/dist/cron-evaluator.js.map +1 -0
- package/dist/cron-fire-marker.d.ts +24 -0
- package/dist/cron-fire-marker.js +25 -0
- package/dist/cron-fire-marker.js.map +1 -0
- package/dist/cron-scheduler.d.ts +46 -0
- package/dist/cron-scheduler.js +114 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/cron-store.d.ts +62 -0
- package/dist/cron-store.js +63 -0
- package/dist/cron-store.js.map +1 -0
- package/dist/cron-tool.d.ts +44 -0
- package/dist/cron-tool.js +151 -0
- package/dist/cron-tool.js.map +1 -0
- package/dist/cwd-resolver.d.ts +72 -0
- package/dist/cwd-resolver.js +166 -0
- package/dist/cwd-resolver.js.map +1 -0
- package/dist/db-adapter.d.ts +21 -0
- package/dist/db-adapter.js +64 -0
- package/dist/db-adapter.js.map +1 -0
- package/dist/file-inline-wrap.d.ts +94 -0
- package/dist/file-inline-wrap.js +243 -0
- package/dist/file-inline-wrap.js.map +1 -0
- package/dist/gateway.d.ts +105 -0
- package/dist/gateway.js +425 -0
- package/dist/gateway.js.map +1 -0
- package/dist/group-config.d.ts +41 -0
- package/dist/group-config.js +104 -0
- package/dist/group-config.js.map +1 -0
- package/dist/group-context.d.ts +81 -0
- package/dist/group-context.js +466 -0
- package/dist/group-context.js.map +1 -0
- package/dist/inbound.d.ts +136 -0
- package/dist/inbound.js +667 -0
- package/dist/inbound.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +1026 -0
- package/dist/index.js.map +1 -0
- package/dist/media-inbound.d.ts +38 -0
- package/dist/media-inbound.js +131 -0
- package/dist/media-inbound.js.map +1 -0
- package/dist/mention-utils.d.ts +108 -0
- package/dist/mention-utils.js +199 -0
- package/dist/mention-utils.js.map +1 -0
- package/dist/octo/api.d.ts +148 -0
- package/dist/octo/api.js +320 -0
- package/dist/octo/api.js.map +1 -0
- package/dist/octo/socket.d.ts +102 -0
- package/dist/octo/socket.js +793 -0
- package/dist/octo/socket.js.map +1 -0
- package/dist/octo/types.d.ts +126 -0
- package/dist/octo/types.js +35 -0
- package/dist/octo/types.js.map +1 -0
- package/dist/prompt-safety.d.ts +78 -0
- package/dist/prompt-safety.js +148 -0
- package/dist/prompt-safety.js.map +1 -0
- package/dist/session-router.d.ts +144 -0
- package/dist/session-router.js +490 -0
- package/dist/session-router.js.map +1 -0
- package/dist/session-store.d.ts +89 -0
- package/dist/session-store.js +297 -0
- package/dist/session-store.js.map +1 -0
- package/dist/skill-linker.d.ts +31 -0
- package/dist/skill-linker.js +160 -0
- package/dist/skill-linker.js.map +1 -0
- package/dist/stream-relay.d.ts +42 -0
- package/dist/stream-relay.js +243 -0
- package/dist/stream-relay.js.map +1 -0
- package/dist/url-policy.d.ts +103 -0
- package/dist/url-policy.js +290 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
// Forked from openclaw-channel-octo v1.0.13 (2026-06-04)
|
|
2
|
+
// Source: https://github.com/Mininglamp-OSS/openclaw-channel-octo
|
|
3
|
+
// Changes: Exported Encoder and Decoder classes for protocol testing.
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
import WebSocket from "ws";
|
|
6
|
+
import { generateKeyPair, sharedKey } from "curve25519-js";
|
|
7
|
+
import { Buffer } from "buffer";
|
|
8
|
+
import CryptoJS from "crypto-js";
|
|
9
|
+
import { Md5 } from "md5-typescript";
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
const PROTO_VERSION = 4;
|
|
12
|
+
/**
|
|
13
|
+
* Maximum bytes allowed in the WebSocket inbound assembly buffer.
|
|
14
|
+
* D1/S6 (齐 P0-1): a malicious server can send partial packets indefinitely
|
|
15
|
+
* (e.g. an unending variable-length encoding) and OOM the bot. 1 MiB is
|
|
16
|
+
* >> any legitimate single packet — if we cross it, close + reconnect.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_TEMP_BUFFER_BYTES = 1 * 1024 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* Maximum bytes used to encode a single variable-length integer (MQTT spec).
|
|
21
|
+
* D1/S6: refuse > 4 continuation bytes — anything longer is malformed and
|
|
22
|
+
* keeps tempBuffer filling forever.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_VARLEN_BYTES = 4;
|
|
25
|
+
/**
|
|
26
|
+
* Per-message decrypt/parse failure cap. After this many failed attempts on the
|
|
27
|
+
* SAME messageID, ack-and-drop it so a single poison (corrupt / non-JSON)
|
|
28
|
+
* payload cannot wedge the stream via infinite server redelivery. A transient
|
|
29
|
+
* failure (< cap) is left un-acked so the server retries.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_DECRYPT_RETRIES = 3;
|
|
32
|
+
/** Cap on distinct messageIDs tracked for decrypt failures (memory bound). */
|
|
33
|
+
const MAX_DECRYPT_FAIL_ENTRIES = 1000;
|
|
34
|
+
// ─── Binary Encoder / Decoder ───────────────────────────────────────────────
|
|
35
|
+
export class Encoder {
|
|
36
|
+
w = [];
|
|
37
|
+
writeByte(b) { this.w.push(b & 0xff); }
|
|
38
|
+
writeBytes(b) { for (let i = 0; i < b.length; i++)
|
|
39
|
+
this.w[this.w.length] = b[i]; }
|
|
40
|
+
writeInt16(b) { this.w.push((b >> 8) & 0xff, b & 0xff); }
|
|
41
|
+
writeInt32(b) { this.w.push((b >> 24) & 0xff, (b >> 16) & 0xff, (b >> 8) & 0xff, b & 0xff); }
|
|
42
|
+
writeInt64(n) {
|
|
43
|
+
const hi = Number(n >> 32n);
|
|
44
|
+
const lo = Number(n & 0xffffffffn);
|
|
45
|
+
this.writeInt32(hi);
|
|
46
|
+
this.writeInt32(lo);
|
|
47
|
+
}
|
|
48
|
+
writeString(s) {
|
|
49
|
+
if (s && s.length > 0) {
|
|
50
|
+
const arr = stringToUint(s);
|
|
51
|
+
this.writeInt16(arr.length);
|
|
52
|
+
for (let i = 0; i < arr.length; i++)
|
|
53
|
+
this.w[this.w.length] = arr[i];
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.writeInt16(0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
toUint8Array() { return new Uint8Array(this.w); }
|
|
60
|
+
}
|
|
61
|
+
export class Decoder {
|
|
62
|
+
data;
|
|
63
|
+
offset = 0;
|
|
64
|
+
constructor(data) {
|
|
65
|
+
this.data = data;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Bounds guard: every reader calls this before touching the buffer so a
|
|
69
|
+
* truncated/malformed packet throws a typed error instead of silently reading
|
|
70
|
+
* `undefined` (which coerces to 0/NaN and produces corrupt parses — wrong
|
|
71
|
+
* messageID/seq → ack mismatch, message loss/dup). The packet-decode caller
|
|
72
|
+
* wraps decode in try/catch, so a throw cleanly rejects the bad packet.
|
|
73
|
+
*/
|
|
74
|
+
require(n) {
|
|
75
|
+
if (this.offset + n > this.data.length) {
|
|
76
|
+
throw new RangeError(`WKDecoder: out-of-bounds read (need ${n} byte(s) at offset ${this.offset}, have ${this.data.length})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
readByte() { this.require(1); return this.data[this.offset++]; }
|
|
80
|
+
readInt16() {
|
|
81
|
+
this.require(2);
|
|
82
|
+
const v = (this.data[this.offset] << 8) | this.data[this.offset + 1];
|
|
83
|
+
this.offset += 2;
|
|
84
|
+
return v;
|
|
85
|
+
}
|
|
86
|
+
readInt32() {
|
|
87
|
+
this.require(4);
|
|
88
|
+
const v = (this.data[this.offset] << 24) |
|
|
89
|
+
(this.data[this.offset + 1] << 16) |
|
|
90
|
+
(this.data[this.offset + 2] << 8) |
|
|
91
|
+
this.data[this.offset + 3];
|
|
92
|
+
this.offset += 4;
|
|
93
|
+
return v >>> 0; // unsigned
|
|
94
|
+
}
|
|
95
|
+
readInt64String() {
|
|
96
|
+
// Read 8 bytes as a big-endian unsigned integer string
|
|
97
|
+
this.require(8);
|
|
98
|
+
let n = BigInt(0);
|
|
99
|
+
for (let i = 0; i < 8; i++) {
|
|
100
|
+
n = (n << 8n) | BigInt(this.data[this.offset + i]);
|
|
101
|
+
}
|
|
102
|
+
this.offset += 8;
|
|
103
|
+
return n.toString();
|
|
104
|
+
}
|
|
105
|
+
readInt64BigInt() {
|
|
106
|
+
this.require(8);
|
|
107
|
+
let n = BigInt(0);
|
|
108
|
+
for (let i = 0; i < 8; i++) {
|
|
109
|
+
n = (n << 8n) | BigInt(this.data[this.offset + i]);
|
|
110
|
+
}
|
|
111
|
+
this.offset += 8;
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
readString() {
|
|
115
|
+
const len = this.readInt16();
|
|
116
|
+
if (len <= 0)
|
|
117
|
+
return "";
|
|
118
|
+
// Guard the declared length against the remaining buffer so an oversized
|
|
119
|
+
// length field can't over-read (slice() would silently short-return and
|
|
120
|
+
// leave the offset past the end, corrupting every subsequent field).
|
|
121
|
+
this.require(len);
|
|
122
|
+
const slice = this.data.slice(this.offset, this.offset + len);
|
|
123
|
+
this.offset += len;
|
|
124
|
+
return uintToString(Array.from(slice));
|
|
125
|
+
}
|
|
126
|
+
readRemaining() {
|
|
127
|
+
const d = this.data.slice(this.offset);
|
|
128
|
+
this.offset = this.data.length;
|
|
129
|
+
return d;
|
|
130
|
+
}
|
|
131
|
+
readVariableLength() {
|
|
132
|
+
let multiplier = 0;
|
|
133
|
+
let rLength = 0;
|
|
134
|
+
while (multiplier < 27) {
|
|
135
|
+
const b = this.readByte();
|
|
136
|
+
rLength = rLength | ((b & 127) << multiplier);
|
|
137
|
+
if ((b & 128) === 0)
|
|
138
|
+
break;
|
|
139
|
+
multiplier += 7;
|
|
140
|
+
}
|
|
141
|
+
return rLength;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function stringToUint(str) {
|
|
145
|
+
return Array.from(new TextEncoder().encode(str));
|
|
146
|
+
}
|
|
147
|
+
function uintToString(array) {
|
|
148
|
+
return new TextDecoder().decode(new Uint8Array(array));
|
|
149
|
+
}
|
|
150
|
+
function encodeVariableLength(len) {
|
|
151
|
+
if (len === 0)
|
|
152
|
+
return [0];
|
|
153
|
+
const ret = [];
|
|
154
|
+
while (len > 0) {
|
|
155
|
+
let digit = len % 0x80;
|
|
156
|
+
len = Math.floor(len / 0x80);
|
|
157
|
+
if (len > 0)
|
|
158
|
+
digit |= 0x80;
|
|
159
|
+
ret.push(digit);
|
|
160
|
+
}
|
|
161
|
+
return ret;
|
|
162
|
+
}
|
|
163
|
+
// ─── AES-CBC Encryption Helpers ─────────────────────────────────────────────
|
|
164
|
+
function aesDecrypt(data, aesKey, aesIV) {
|
|
165
|
+
const str = Buffer.from(data).toString("binary");
|
|
166
|
+
const ciphertext = CryptoJS.enc.Base64.parse(str);
|
|
167
|
+
const decrypted = CryptoJS.AES.decrypt(CryptoJS.enc.Base64.stringify(ciphertext), CryptoJS.enc.Utf8.parse(aesKey), {
|
|
168
|
+
keySize: 128 / 8,
|
|
169
|
+
iv: CryptoJS.enc.Utf8.parse(aesIV),
|
|
170
|
+
mode: CryptoJS.mode.CBC,
|
|
171
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
172
|
+
});
|
|
173
|
+
return Uint8Array.from(Buffer.from(decrypted.toString(CryptoJS.enc.Utf8)));
|
|
174
|
+
}
|
|
175
|
+
// ─── Packet Encoding / Decoding ─────────────────────────────────────────────
|
|
176
|
+
function encodeConnectPacket(opts) {
|
|
177
|
+
const body = new Encoder();
|
|
178
|
+
body.writeByte(opts.version);
|
|
179
|
+
body.writeByte(opts.deviceFlag);
|
|
180
|
+
body.writeString(opts.deviceID);
|
|
181
|
+
body.writeString(opts.uid);
|
|
182
|
+
body.writeString(opts.token);
|
|
183
|
+
body.writeInt64(BigInt(opts.clientTimestamp));
|
|
184
|
+
body.writeString(opts.clientKey);
|
|
185
|
+
const bodyBytes = Array.from(body.toUint8Array());
|
|
186
|
+
const frame = new Encoder();
|
|
187
|
+
// header: packetType << 4 | flags
|
|
188
|
+
frame.writeByte((1 /* PacketType.CONNECT */ << 4) | 0);
|
|
189
|
+
frame.writeBytes(encodeVariableLength(bodyBytes.length));
|
|
190
|
+
frame.writeBytes(bodyBytes);
|
|
191
|
+
return frame.toUint8Array();
|
|
192
|
+
}
|
|
193
|
+
function encodePingPacket() {
|
|
194
|
+
return new Uint8Array([(7 /* PacketType.PING */ << 4) | 0]);
|
|
195
|
+
}
|
|
196
|
+
function encodeRecvackPacket(messageID, messageSeq) {
|
|
197
|
+
const body = new Encoder();
|
|
198
|
+
body.writeInt64(BigInt(messageID));
|
|
199
|
+
body.writeInt32(messageSeq);
|
|
200
|
+
const bodyBytes = Array.from(body.toUint8Array());
|
|
201
|
+
const frame = new Encoder();
|
|
202
|
+
frame.writeByte((6 /* PacketType.RECVACK */ << 4) | 0);
|
|
203
|
+
frame.writeBytes(encodeVariableLength(bodyBytes.length));
|
|
204
|
+
frame.writeBytes(bodyBytes);
|
|
205
|
+
return frame.toUint8Array();
|
|
206
|
+
}
|
|
207
|
+
function parseSettingByte(v) {
|
|
208
|
+
return {
|
|
209
|
+
receiptEnabled: ((v >> 7) & 0x01) > 0,
|
|
210
|
+
topic: ((v >> 3) & 0x01) > 0,
|
|
211
|
+
streamOn: ((v >> 1) & 0x01) > 0,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* WuKongIM WebSocket client for bot connections.
|
|
216
|
+
*
|
|
217
|
+
* Implements the WuKongIM binary protocol directly over WebSocket,
|
|
218
|
+
* with per-instance DH key exchange, AES encryption, heartbeat,
|
|
219
|
+
* reconnect, and RECVACK.
|
|
220
|
+
*
|
|
221
|
+
* Each WKSocket instance maintains its own independent connection,
|
|
222
|
+
* enabling multiple bot accounts to run simultaneously.
|
|
223
|
+
*/
|
|
224
|
+
export class WKSocket extends EventEmitter {
|
|
225
|
+
opts;
|
|
226
|
+
ws = null;
|
|
227
|
+
connected = false;
|
|
228
|
+
needReconnect = true;
|
|
229
|
+
reconnectTimer = null;
|
|
230
|
+
heartTimer = null;
|
|
231
|
+
pingRetryCount = 0;
|
|
232
|
+
pingMaxRetry = 3;
|
|
233
|
+
reconnectAttempts = 0;
|
|
234
|
+
stableTimer = null;
|
|
235
|
+
lastConnectTime = 0;
|
|
236
|
+
rapidDisconnectCount = 0;
|
|
237
|
+
// Per-instance crypto state (set after CONNACK)
|
|
238
|
+
aesKey = "";
|
|
239
|
+
aesIV = "";
|
|
240
|
+
dhPrivateKey = null;
|
|
241
|
+
serverVersion = 0;
|
|
242
|
+
// Buffer for handling packet fragmentation (sticky packets)
|
|
243
|
+
tempBuffer = [];
|
|
244
|
+
// Per-message decrypt-failure counts (by messageID). After
|
|
245
|
+
// MAX_DECRYPT_RETRIES, a poison message is ack'd-and-dropped so the server
|
|
246
|
+
// stops redelivering it forever (a single corrupt/non-JSON payload must not
|
|
247
|
+
// wedge the stream). Bounded to avoid unbounded growth from many distinct ids.
|
|
248
|
+
decryptFailCounts = new Map();
|
|
249
|
+
constructor(opts) {
|
|
250
|
+
super();
|
|
251
|
+
this.opts = opts;
|
|
252
|
+
}
|
|
253
|
+
/** Connect to WuKongIM WebSocket */
|
|
254
|
+
connect() {
|
|
255
|
+
this.needReconnect = true;
|
|
256
|
+
this.doConnect();
|
|
257
|
+
}
|
|
258
|
+
/** Gracefully disconnect */
|
|
259
|
+
disconnect() {
|
|
260
|
+
this.needReconnect = false;
|
|
261
|
+
this.connected = false;
|
|
262
|
+
this.lastConnectTime = 0;
|
|
263
|
+
this.rapidDisconnectCount = 0;
|
|
264
|
+
this.stopHeart();
|
|
265
|
+
this.stopReconnectTimer();
|
|
266
|
+
this.clearStableTimer();
|
|
267
|
+
if (this.ws) {
|
|
268
|
+
try {
|
|
269
|
+
this.ws.removeAllListeners();
|
|
270
|
+
this.ws.close();
|
|
271
|
+
}
|
|
272
|
+
catch { /* ignore */ }
|
|
273
|
+
this.ws = null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Disconnect and wait for the old WS to fully close before resolving. */
|
|
277
|
+
async disconnectAndWait(timeoutMs = 2000) {
|
|
278
|
+
this.needReconnect = false;
|
|
279
|
+
this.connected = false;
|
|
280
|
+
this.stopHeart();
|
|
281
|
+
this.stopReconnectTimer();
|
|
282
|
+
this.clearStableTimer();
|
|
283
|
+
const oldWs = this.ws;
|
|
284
|
+
this.ws = null;
|
|
285
|
+
this.lastConnectTime = 0;
|
|
286
|
+
this.rapidDisconnectCount = 0;
|
|
287
|
+
if (!oldWs)
|
|
288
|
+
return;
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
let resolved = false;
|
|
291
|
+
const done = () => {
|
|
292
|
+
if (resolved)
|
|
293
|
+
return;
|
|
294
|
+
resolved = true;
|
|
295
|
+
oldWs.removeAllListeners();
|
|
296
|
+
resolve();
|
|
297
|
+
};
|
|
298
|
+
oldWs.on("close", done);
|
|
299
|
+
try {
|
|
300
|
+
oldWs.close();
|
|
301
|
+
}
|
|
302
|
+
catch { /* ignore */ }
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
if (!resolved) {
|
|
305
|
+
try {
|
|
306
|
+
oldWs.terminate?.();
|
|
307
|
+
}
|
|
308
|
+
catch { /* ignore */ }
|
|
309
|
+
done();
|
|
310
|
+
}
|
|
311
|
+
}, timeoutMs);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// ─── Internal Connection Logic ──────────────────────────────────────────
|
|
315
|
+
doConnect() {
|
|
316
|
+
this.clearStableTimer();
|
|
317
|
+
if (this.ws) {
|
|
318
|
+
try {
|
|
319
|
+
this.ws.close();
|
|
320
|
+
}
|
|
321
|
+
catch { /* ignore */ }
|
|
322
|
+
this.ws = null;
|
|
323
|
+
}
|
|
324
|
+
this.tempBuffer = [];
|
|
325
|
+
const ws = new WebSocket(this.opts.wsUrl);
|
|
326
|
+
ws.binaryType = "arraybuffer";
|
|
327
|
+
this.ws = ws;
|
|
328
|
+
ws.on("open", () => {
|
|
329
|
+
if (this.ws !== ws)
|
|
330
|
+
return; // stale guard
|
|
331
|
+
this.tempBuffer = [];
|
|
332
|
+
// Generate DH key pair
|
|
333
|
+
const seed = randomBytes(32);
|
|
334
|
+
const keyPair = generateKeyPair(seed);
|
|
335
|
+
this.dhPrivateKey = keyPair.private;
|
|
336
|
+
const pubKey = Buffer.from(keyPair.public).toString("base64");
|
|
337
|
+
const deviceID = generateDeviceID() + "W";
|
|
338
|
+
const packet = encodeConnectPacket({
|
|
339
|
+
version: PROTO_VERSION,
|
|
340
|
+
deviceFlag: 0, // 0 = app/bot
|
|
341
|
+
deviceID,
|
|
342
|
+
uid: this.opts.uid,
|
|
343
|
+
token: this.opts.token,
|
|
344
|
+
clientTimestamp: Date.now(),
|
|
345
|
+
clientKey: pubKey,
|
|
346
|
+
});
|
|
347
|
+
ws.send(packet);
|
|
348
|
+
});
|
|
349
|
+
ws.on("message", (data) => {
|
|
350
|
+
if (this.ws !== ws)
|
|
351
|
+
return; // stale guard
|
|
352
|
+
// Buffer is already a Uint8Array view; use it directly to avoid the
|
|
353
|
+
// 3-arg vs 1-arg footgun. `new Uint8Array(buffer)` without byteOffset/
|
|
354
|
+
// byteLength reads the WHOLE underlying ArrayBuffer, which for a Buffer
|
|
355
|
+
// that is a view (e.g. from a buffer pool) leaks adjacent memory into
|
|
356
|
+
// the frame parser.
|
|
357
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
358
|
+
this.handleRawData(bytes);
|
|
359
|
+
});
|
|
360
|
+
ws.on("close", () => {
|
|
361
|
+
// Ignore close events from stale WebSocket instances.
|
|
362
|
+
// When onError triggers disconnect()+connect(), the old WS close event
|
|
363
|
+
// fires asynchronously and must not trigger a phantom reconnect.
|
|
364
|
+
if (this.ws !== ws)
|
|
365
|
+
return;
|
|
366
|
+
if (this.connected) {
|
|
367
|
+
this.connected = false;
|
|
368
|
+
this.opts.onDisconnected?.();
|
|
369
|
+
}
|
|
370
|
+
this.stopHeart();
|
|
371
|
+
this.clearStableTimer();
|
|
372
|
+
// Track rapid disconnects: if connection lasted <5s, it's unstable
|
|
373
|
+
if (this.lastConnectTime > 0) {
|
|
374
|
+
const duration = Date.now() - this.lastConnectTime;
|
|
375
|
+
if (duration < 5000) {
|
|
376
|
+
this.rapidDisconnectCount++;
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
this.rapidDisconnectCount = 0;
|
|
380
|
+
}
|
|
381
|
+
this.lastConnectTime = 0;
|
|
382
|
+
}
|
|
383
|
+
// If 3+ consecutive rapid disconnects, trigger onError for token refresh
|
|
384
|
+
if (this.rapidDisconnectCount >= 3) {
|
|
385
|
+
this.needReconnect = false;
|
|
386
|
+
this.rapidDisconnectCount = 0;
|
|
387
|
+
this.opts.onError?.(new Error("Connect failed: rapid disconnect detected"));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (this.needReconnect) {
|
|
391
|
+
this.scheduleReconnect();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
ws.on("error", (err) => {
|
|
395
|
+
if (this.ws !== ws)
|
|
396
|
+
return; // stale guard
|
|
397
|
+
console.debug("[WKSocket] ws error:", err.message);
|
|
398
|
+
// The 'close' event will follow, which handles reconnect
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
scheduleReconnect() {
|
|
402
|
+
this.stopReconnectTimer();
|
|
403
|
+
const baseDelay = 3000;
|
|
404
|
+
const maxDelay = 60000;
|
|
405
|
+
const exponentialDelay = Math.min(baseDelay * Math.pow(2, this.reconnectAttempts), maxDelay);
|
|
406
|
+
// Add ±25% random jitter to prevent thundering herd
|
|
407
|
+
const jitter = exponentialDelay * (0.75 + Math.random() * 0.5);
|
|
408
|
+
const delay = Math.floor(jitter);
|
|
409
|
+
this.reconnectAttempts++;
|
|
410
|
+
this.reconnectTimer = setTimeout(() => {
|
|
411
|
+
if (this.needReconnect) {
|
|
412
|
+
this.doConnect();
|
|
413
|
+
}
|
|
414
|
+
}, delay);
|
|
415
|
+
}
|
|
416
|
+
stopReconnectTimer() {
|
|
417
|
+
if (this.reconnectTimer) {
|
|
418
|
+
clearTimeout(this.reconnectTimer);
|
|
419
|
+
this.reconnectTimer = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
startStableTimer() {
|
|
423
|
+
this.clearStableTimer();
|
|
424
|
+
this.stableTimer = setTimeout(() => {
|
|
425
|
+
if (this.connected) {
|
|
426
|
+
this.reconnectAttempts = 0;
|
|
427
|
+
this.rapidDisconnectCount = 0;
|
|
428
|
+
}
|
|
429
|
+
}, 30_000);
|
|
430
|
+
}
|
|
431
|
+
clearStableTimer() {
|
|
432
|
+
if (this.stableTimer) {
|
|
433
|
+
clearTimeout(this.stableTimer);
|
|
434
|
+
this.stableTimer = null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// ─── Heartbeat ──────────────────────────────────────────────────────────
|
|
438
|
+
restartHeart() {
|
|
439
|
+
this.stopHeart();
|
|
440
|
+
this.pingRetryCount = 0;
|
|
441
|
+
this.heartTimer = setInterval(() => {
|
|
442
|
+
this.pingRetryCount++;
|
|
443
|
+
if (this.pingRetryCount > this.pingMaxRetry) {
|
|
444
|
+
console.debug("[WKSocket] ping timeout, reconnecting...");
|
|
445
|
+
this.stopHeart();
|
|
446
|
+
this.clearStableTimer();
|
|
447
|
+
if (this.ws) {
|
|
448
|
+
try {
|
|
449
|
+
this.ws.close();
|
|
450
|
+
}
|
|
451
|
+
catch { /* ignore */ }
|
|
452
|
+
this.ws = null;
|
|
453
|
+
}
|
|
454
|
+
if (this.connected) {
|
|
455
|
+
this.connected = false;
|
|
456
|
+
this.opts.onDisconnected?.();
|
|
457
|
+
}
|
|
458
|
+
if (this.needReconnect) {
|
|
459
|
+
this.scheduleReconnect();
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
this.sendRaw(encodePingPacket());
|
|
464
|
+
}, 60_000); // 60s heartbeat interval (matches SDK default)
|
|
465
|
+
}
|
|
466
|
+
stopHeart() {
|
|
467
|
+
if (this.heartTimer) {
|
|
468
|
+
clearInterval(this.heartTimer);
|
|
469
|
+
this.heartTimer = null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// ─── Raw Data & Packet Framing ──────────────────────────────────────────
|
|
473
|
+
sendRaw(data) {
|
|
474
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
475
|
+
this.ws.send(data);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
handleRawData(data) {
|
|
479
|
+
for (let i = 0; i < data.length; i++)
|
|
480
|
+
this.tempBuffer.push(data[i]);
|
|
481
|
+
// D1/S6 (齐 P0-1): cap tempBuffer at MAX_TEMP_BUFFER_BYTES to prevent
|
|
482
|
+
// unbounded growth from a malicious/buggy server that sends partial
|
|
483
|
+
// packets indefinitely (e.g. an unending variable-length encoding).
|
|
484
|
+
if (this.tempBuffer.length > MAX_TEMP_BUFFER_BYTES) {
|
|
485
|
+
console.error(`[WKSocket] tempBuffer exceeded ${MAX_TEMP_BUFFER_BYTES} bytes (got ${this.tempBuffer.length}) — dropping and reconnecting`);
|
|
486
|
+
this.tempBuffer = [];
|
|
487
|
+
if (this.ws) {
|
|
488
|
+
try {
|
|
489
|
+
this.ws.close();
|
|
490
|
+
}
|
|
491
|
+
catch { /* ignore */ }
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
// Parse complete packets using a moving cursor instead of re-slicing the
|
|
497
|
+
// whole buffer per packet. The old `tempBuffer = tempBuffer.slice(total)`
|
|
498
|
+
// per iteration was O(n²): a peer dribbling many tiny frames near the 1 MiB
|
|
499
|
+
// cap forced ~n full-array copies of an ~n-element array, stalling the event
|
|
500
|
+
// loop (shared across bots) without exceeding the byte cap. Now we advance
|
|
501
|
+
// an offset and trim the consumed prefix exactly once at the end.
|
|
502
|
+
let consumed = 0;
|
|
503
|
+
for (;;) {
|
|
504
|
+
const used = this.unpackOne(this.tempBuffer, consumed);
|
|
505
|
+
if (used === 0)
|
|
506
|
+
break; // incomplete packet — wait for more bytes
|
|
507
|
+
consumed += used;
|
|
508
|
+
}
|
|
509
|
+
if (consumed > 0) {
|
|
510
|
+
this.tempBuffer = this.tempBuffer.slice(consumed);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
console.debug("[WKSocket] decode error:", err);
|
|
515
|
+
// Reset buffer and reconnect
|
|
516
|
+
this.tempBuffer = [];
|
|
517
|
+
if (this.ws) {
|
|
518
|
+
try {
|
|
519
|
+
this.ws.close();
|
|
520
|
+
}
|
|
521
|
+
catch { /* ignore */ }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Parse ONE packet starting at `start` in `data`. Returns the number of bytes
|
|
527
|
+
* consumed, or 0 if the buffer does not yet hold a complete packet (caller
|
|
528
|
+
* should wait for more bytes). Reads via `start + offset` and slices only the
|
|
529
|
+
* single packet's bytes — never the whole buffer — so repeated calls over a
|
|
530
|
+
* large buffer stay O(n) total, not O(n²).
|
|
531
|
+
*/
|
|
532
|
+
unpackOne(data, start) {
|
|
533
|
+
const available = data.length - start;
|
|
534
|
+
if (available <= 0)
|
|
535
|
+
return 0;
|
|
536
|
+
const header = data[start];
|
|
537
|
+
const packetType = header >> 4;
|
|
538
|
+
// PONG is a single byte
|
|
539
|
+
if (packetType === 8 /* PacketType.PONG */) {
|
|
540
|
+
this.onPong();
|
|
541
|
+
return 1;
|
|
542
|
+
}
|
|
543
|
+
// PING from server (shouldn't happen but handle gracefully)
|
|
544
|
+
if (packetType === 7 /* PacketType.PING */) {
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
const fixedHeaderLength = 1;
|
|
548
|
+
let pos = start + fixedHeaderLength;
|
|
549
|
+
let remLength = 0;
|
|
550
|
+
let multiplier = 1;
|
|
551
|
+
let hasMore = false;
|
|
552
|
+
let remLengthFull = true;
|
|
553
|
+
do {
|
|
554
|
+
if (pos > data.length - 1) {
|
|
555
|
+
remLengthFull = false;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
// D1/S6 (齐 P0-1): cap at MAX_VARLEN_BYTES per MQTT spec. Without
|
|
559
|
+
// this, a stream of 0x80 bytes would never terminate and would let
|
|
560
|
+
// tempBuffer grow until handleRawData kills the connection — raise
|
|
561
|
+
// earlier and more explicitly here.
|
|
562
|
+
if (pos - (start + fixedHeaderLength) >= MAX_VARLEN_BYTES) {
|
|
563
|
+
throw new Error(`[WKSocket] variable-length encoding exceeded ${MAX_VARLEN_BYTES} bytes — malformed packet`);
|
|
564
|
+
}
|
|
565
|
+
const digit = data[pos++];
|
|
566
|
+
remLength += (digit & 127) * multiplier;
|
|
567
|
+
multiplier *= 128;
|
|
568
|
+
hasMore = (digit & 0x80) !== 0;
|
|
569
|
+
} while (hasMore);
|
|
570
|
+
if (!remLengthFull)
|
|
571
|
+
return 0; // Incomplete frame — need more bytes
|
|
572
|
+
const remLengthLength = pos - (start + fixedHeaderLength);
|
|
573
|
+
const totalLength = fixedHeaderLength + remLengthLength + remLength;
|
|
574
|
+
if (totalLength > available)
|
|
575
|
+
return 0; // Incomplete packet — need more bytes
|
|
576
|
+
// Extract exactly this one packet's bytes and dispatch.
|
|
577
|
+
const packetData = new Uint8Array(data.slice(start, start + totalLength));
|
|
578
|
+
this.onPacket(packetData);
|
|
579
|
+
return totalLength;
|
|
580
|
+
}
|
|
581
|
+
// ─── Packet Handling ────────────────────────────────────────────────────
|
|
582
|
+
onPong() {
|
|
583
|
+
this.pingRetryCount = 0;
|
|
584
|
+
}
|
|
585
|
+
onPacket(data) {
|
|
586
|
+
const firstByte = data[0];
|
|
587
|
+
const packetType = firstByte >> 4;
|
|
588
|
+
// Skip the header and variable-length bytes to get body
|
|
589
|
+
const dec = new Decoder(data);
|
|
590
|
+
dec.readByte(); // header byte
|
|
591
|
+
if (packetType !== 7 /* PacketType.PING */ && packetType !== 8 /* PacketType.PONG */) {
|
|
592
|
+
dec.readVariableLength(); // remaining length
|
|
593
|
+
}
|
|
594
|
+
// WuKongIM header byte layout: [packetType:4][flags:4]
|
|
595
|
+
// Bit 0 of flags has DIFFERENT semantics per packet type:
|
|
596
|
+
// CONNACK: bit 0 = hasServerVersion (server includes version byte in body)
|
|
597
|
+
// RECV: bit 0 = noPersist (message should not be persisted)
|
|
598
|
+
// Bit 1 of flags:
|
|
599
|
+
// RECV: bit 1 = reddot (show unread badge)
|
|
600
|
+
switch (packetType) {
|
|
601
|
+
case 2 /* PacketType.CONNACK */: {
|
|
602
|
+
const hasServerVersion = (firstByte & 0x01) > 0;
|
|
603
|
+
this.onConnack(dec, hasServerVersion);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
case 5 /* PacketType.RECV */: {
|
|
607
|
+
const _noPersist = (firstByte & 0x01) > 0;
|
|
608
|
+
const _reddot = ((firstByte >> 1) & 0x01) > 0;
|
|
609
|
+
this.onRecv(dec, _noPersist, _reddot);
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
case 9 /* PacketType.DISCONNECT */:
|
|
613
|
+
this.onDisconnect(dec);
|
|
614
|
+
break;
|
|
615
|
+
case 4 /* PacketType.SENDACK */:
|
|
616
|
+
// We don't send messages via WS, ignore
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
onConnack(dec, hasServerVersion) {
|
|
621
|
+
if (hasServerVersion) {
|
|
622
|
+
this.serverVersion = dec.readByte();
|
|
623
|
+
}
|
|
624
|
+
dec.readInt64BigInt(); // timeDiff (unused)
|
|
625
|
+
const reasonCode = dec.readByte();
|
|
626
|
+
const serverKey = dec.readString();
|
|
627
|
+
const salt = dec.readString();
|
|
628
|
+
if (this.serverVersion >= 4) {
|
|
629
|
+
dec.readInt64BigInt(); // nodeId (unused)
|
|
630
|
+
}
|
|
631
|
+
if (reasonCode === 1) {
|
|
632
|
+
// Success — derive AES key from DH shared secret.
|
|
633
|
+
// A malformed/short salt yields a wrong AES-CBC IV (CryptoJS zero-pads it),
|
|
634
|
+
// which makes EVERY subsequent payload decrypt fail — i.e. a bot that looks
|
|
635
|
+
// connected (heartbeat fine) but silently drops every message. Fail the
|
|
636
|
+
// handshake instead so we reconnect and re-derive, rather than entering
|
|
637
|
+
// that silent-drop state. (serverKey is validated the same way: a bad DH
|
|
638
|
+
// key throws in sharedKey(), caught by handleRawData's try/catch.)
|
|
639
|
+
// The IV needs 16 BYTES, so validate by byte length, not char length: a
|
|
640
|
+
// 16-char salt with multibyte UTF-8 chars is <16 OR >16 bytes and would
|
|
641
|
+
// yield a wrong IV (the same silent-decrypt-failure this guard prevents).
|
|
642
|
+
const saltByteLen = salt ? Buffer.byteLength(salt, "utf8") : 0;
|
|
643
|
+
if (saltByteLen < 16) {
|
|
644
|
+
this.connected = false;
|
|
645
|
+
console.error(`[WKSocket] CONNACK salt too short (got ${saltByteLen} bytes, need >=16) — ` +
|
|
646
|
+
`AES IV would be invalid and every message would silently fail to decrypt. ` +
|
|
647
|
+
`Failing the connection to force a fresh handshake.`);
|
|
648
|
+
if (this.ws) {
|
|
649
|
+
try {
|
|
650
|
+
this.ws.close();
|
|
651
|
+
}
|
|
652
|
+
catch { /* ignore */ }
|
|
653
|
+
}
|
|
654
|
+
// needReconnect stays true (default) so the close handler reconnects.
|
|
655
|
+
this.opts.onError?.(new Error("CONNACK salt too short"));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const serverPubKey = Uint8Array.from(Buffer.from(serverKey, "base64"));
|
|
659
|
+
const secret = sharedKey(this.dhPrivateKey, serverPubKey);
|
|
660
|
+
const secretBase64 = Buffer.from(secret).toString("base64");
|
|
661
|
+
const aesKeyFull = Md5.init(secretBase64);
|
|
662
|
+
this.aesKey = aesKeyFull.substring(0, 16);
|
|
663
|
+
// Take the first 16 BYTES of the salt as the IV (CryptoJS Utf8.parse(aesIV)
|
|
664
|
+
// re-encodes to bytes, so a 16-byte ASCII-equivalent slice is required).
|
|
665
|
+
this.aesIV = Buffer.from(salt, "utf8").subarray(0, 16).toString("latin1");
|
|
666
|
+
this.connected = true;
|
|
667
|
+
this.lastConnectTime = Date.now();
|
|
668
|
+
this.restartHeart();
|
|
669
|
+
this.startStableTimer();
|
|
670
|
+
this.opts.onConnected?.();
|
|
671
|
+
}
|
|
672
|
+
else if (reasonCode === 0) {
|
|
673
|
+
// Kicked
|
|
674
|
+
this.connected = false;
|
|
675
|
+
this.needReconnect = false;
|
|
676
|
+
if (this.ws) {
|
|
677
|
+
try {
|
|
678
|
+
this.ws.close();
|
|
679
|
+
}
|
|
680
|
+
catch { }
|
|
681
|
+
this.ws = null;
|
|
682
|
+
}
|
|
683
|
+
this.opts.onError?.(new Error("Kicked by server"));
|
|
684
|
+
this.opts.onDisconnected?.();
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
// Connect failed
|
|
688
|
+
this.connected = false;
|
|
689
|
+
this.needReconnect = false;
|
|
690
|
+
this.opts.onError?.(new Error(`Connect failed: reasonCode=${reasonCode}`));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
694
|
+
onRecv(dec, _noPersist, _reddot) {
|
|
695
|
+
const settingByte = dec.readByte();
|
|
696
|
+
const setting = parseSettingByte(settingByte);
|
|
697
|
+
dec.readString(); // msgKey (unused)
|
|
698
|
+
const fromUID = dec.readString();
|
|
699
|
+
const channelID = dec.readString();
|
|
700
|
+
const channelType = dec.readByte();
|
|
701
|
+
if (this.serverVersion >= 3) {
|
|
702
|
+
dec.readInt32(); // expire (unused)
|
|
703
|
+
}
|
|
704
|
+
dec.readString(); // clientMsgNo (unused)
|
|
705
|
+
const messageID = dec.readInt64String();
|
|
706
|
+
const messageSeq = dec.readInt32();
|
|
707
|
+
const timestamp = dec.readInt32();
|
|
708
|
+
if (setting.topic) {
|
|
709
|
+
dec.readString(); // topic (unused)
|
|
710
|
+
}
|
|
711
|
+
const encryptedPayload = dec.readRemaining();
|
|
712
|
+
// Decrypt + parse BEFORE acking. RECVACK tells the server "delivered, don't
|
|
713
|
+
// resend" — if we ack first and decrypt then fails (transient key/IV issue),
|
|
714
|
+
// the message is lost forever with only a debug log. By acking only after a
|
|
715
|
+
// successful parse, a transient failure leaves the message un-acked so the
|
|
716
|
+
// server redelivers it. (A *permanent* decrypt failure means the AES key/IV
|
|
717
|
+
// from CONNACK is wrong — onConnack now fails the connection in that case,
|
|
718
|
+
// forcing a fresh handshake rather than a silent-drop or redelivery loop.)
|
|
719
|
+
let payloadObj;
|
|
720
|
+
try {
|
|
721
|
+
const decryptedBytes = aesDecrypt(encryptedPayload, this.aesKey, this.aesIV);
|
|
722
|
+
const payloadStr = uintToString(Array.from(decryptedBytes));
|
|
723
|
+
payloadObj = JSON.parse(payloadStr);
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
// Count failures per messageID. Below the cap, leave it un-acked so the
|
|
727
|
+
// server redelivers (handles a transient hiccup). At the cap, ack-and-drop
|
|
728
|
+
// this one poison message so it can't wedge the stream forever.
|
|
729
|
+
const fails = (this.decryptFailCounts.get(messageID) ?? 0) + 1;
|
|
730
|
+
if (fails >= MAX_DECRYPT_RETRIES) {
|
|
731
|
+
console.error(`[WKSocket] payload decrypt/parse failed ${fails}x for message ${messageID} — ` +
|
|
732
|
+
`ack-and-drop (poison message) so it stops redelivering:`, err);
|
|
733
|
+
this.decryptFailCounts.delete(messageID);
|
|
734
|
+
this.sendRaw(encodeRecvackPacket(messageID, messageSeq));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// Bound the map so a flood of distinct failing ids can't grow it forever.
|
|
738
|
+
if (this.decryptFailCounts.size >= MAX_DECRYPT_FAIL_ENTRIES) {
|
|
739
|
+
this.decryptFailCounts.clear();
|
|
740
|
+
}
|
|
741
|
+
this.decryptFailCounts.set(messageID, fails);
|
|
742
|
+
console.error(`[WKSocket] payload decrypt/parse error (attempt ${fails}/${MAX_DECRYPT_RETRIES}) — ` +
|
|
743
|
+
`NOT acking so the server can redeliver:`, err);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
// Parse succeeded — clear any prior failure count and ack.
|
|
747
|
+
this.decryptFailCounts.delete(messageID);
|
|
748
|
+
this.sendRaw(encodeRecvackPacket(messageID, messageSeq));
|
|
749
|
+
// Build MessagePayload (same shape as SDK's contentObj-based output)
|
|
750
|
+
const payload = {
|
|
751
|
+
type: payloadObj?.type ?? 0,
|
|
752
|
+
content: payloadObj?.content,
|
|
753
|
+
...payloadObj,
|
|
754
|
+
};
|
|
755
|
+
const msg = {
|
|
756
|
+
message_id: messageID,
|
|
757
|
+
message_seq: messageSeq,
|
|
758
|
+
from_uid: fromUID,
|
|
759
|
+
channel_id: channelID,
|
|
760
|
+
channel_type: channelType,
|
|
761
|
+
timestamp,
|
|
762
|
+
payload,
|
|
763
|
+
streamOn: setting.streamOn,
|
|
764
|
+
};
|
|
765
|
+
this.opts.onMessage(msg);
|
|
766
|
+
}
|
|
767
|
+
onDisconnect(dec) {
|
|
768
|
+
dec.readByte(); // reasonCode (unused)
|
|
769
|
+
dec.readString(); // reason (unused)
|
|
770
|
+
this.connected = false;
|
|
771
|
+
this.needReconnect = false;
|
|
772
|
+
this.stopHeart();
|
|
773
|
+
this.clearStableTimer();
|
|
774
|
+
if (this.ws) {
|
|
775
|
+
try {
|
|
776
|
+
this.ws.close();
|
|
777
|
+
}
|
|
778
|
+
catch { }
|
|
779
|
+
this.ws = null;
|
|
780
|
+
}
|
|
781
|
+
this.opts.onError?.(new Error("Kicked by server"));
|
|
782
|
+
this.opts.onDisconnected?.();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
786
|
+
function generateDeviceID() {
|
|
787
|
+
return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
788
|
+
const r = (Math.random() * 16) | 0;
|
|
789
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
790
|
+
return v.toString(16);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
//# sourceMappingURL=socket.js.map
|