@alemonjs/qq-bot 2.1.33 → 2.1.35

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.
Files changed (132) hide show
  1. package/README.md +46 -8
  2. package/dist/assets/index.css +1 -1
  3. package/dist/assets/index.js +12 -5
  4. package/dist/index.html +1 -1
  5. package/lib/config.d.ts +44 -19
  6. package/lib/config.js +39 -35
  7. package/lib/desktop.d.ts +4 -1
  8. package/lib/desktop.js +53 -57
  9. package/lib/format.d.ts +26 -5
  10. package/lib/format.js +84 -101
  11. package/lib/hook.d.ts +82 -65
  12. package/lib/hook.js +35 -32
  13. package/lib/index.d.ts +6 -4
  14. package/lib/index.js +13 -16
  15. package/lib/index.webhook.d.ts +4 -1
  16. package/lib/index.webhook.js +12 -14
  17. package/lib/index.websoket.d.ts +6 -3
  18. package/lib/index.websoket.js +131 -41
  19. package/lib/message/AT_MESSAGE_CREATE.d.ts +36 -33
  20. package/lib/message/AT_MESSAGE_CREATE.js +1 -1
  21. package/lib/message/CHANNEL_CREATE.d.ts +17 -14
  22. package/lib/message/CHANNEL_CREATE.js +1 -1
  23. package/lib/message/CHANNEL_DELETE.d.ts +22 -14
  24. package/lib/message/CHANNEL_DELETE.js +1 -1
  25. package/lib/message/CHANNEL_UPDATE.d.ts +22 -14
  26. package/lib/message/CHANNEL_UPDATE.js +1 -1
  27. package/lib/message/DIRECT_MESSAGE_CREATE.d.ts +35 -27
  28. package/lib/message/DIRECT_MESSAGE_CREATE.js +1 -1
  29. package/lib/message/DIRECT_MESSAGE_DELETE.d.ts +23 -15
  30. package/lib/message/DIRECT_MESSAGE_DELETE.js +1 -1
  31. package/lib/message/ERROR.d.ts +4 -1
  32. package/lib/message/ERROR.js +1 -1
  33. package/lib/message/GUILD_CREATE.d.ts +22 -14
  34. package/lib/message/GUILD_CREATE.js +1 -1
  35. package/lib/message/GUILD_DELETE.d.ts +22 -14
  36. package/lib/message/GUILD_DELETE.js +1 -1
  37. package/lib/message/GUILD_MEMBER_ADD.d.ts +21 -13
  38. package/lib/message/GUILD_MEMBER_ADD.js +1 -1
  39. package/lib/message/GUILD_MEMBER_REMOVE.d.ts +21 -13
  40. package/lib/message/GUILD_MEMBER_REMOVE.js +1 -1
  41. package/lib/message/GUILD_MEMBER_UPDATE.d.ts +21 -13
  42. package/lib/message/GUILD_MEMBER_UPDATE.js +1 -1
  43. package/lib/message/GUILD_UPDATE.d.ts +22 -14
  44. package/lib/message/GUILD_UPDATE.js +1 -1
  45. package/lib/message/INTERACTION_CREATE.d.ts +52 -44
  46. package/lib/message/INTERACTION_CREATE.js +1 -1
  47. package/lib/message/MESSAGE_CREATE.d.ts +12 -1
  48. package/lib/message/MESSAGE_CREATE.js +1 -1
  49. package/lib/message/MESSAGE_DELETE.d.ts +12 -1
  50. package/lib/message/MESSAGE_DELETE.js +1 -1
  51. package/lib/message/MESSAGE_REACTION_ADD.d.ts +15 -12
  52. package/lib/message/MESSAGE_REACTION_ADD.js +1 -1
  53. package/lib/message/MESSAGE_REACTION_REMOVE.d.ts +15 -12
  54. package/lib/message/MESSAGE_REACTION_REMOVE.js +1 -1
  55. package/lib/message/PUBLIC_MESSAGE_DELETE.d.ts +20 -13
  56. package/lib/message/PUBLIC_MESSAGE_DELETE.js +1 -1
  57. package/lib/message/READY.d.ts +11 -5
  58. package/lib/message/READY.js +1 -1
  59. package/lib/message/group/C2C_MESSAGE_CREATE.d.ts +18 -15
  60. package/lib/message/group/C2C_MESSAGE_CREATE.js +1 -1
  61. package/lib/message/group/C2C_MSG_RECEIVE.d.ts +13 -3
  62. package/lib/message/group/C2C_MSG_RECEIVE.js +1 -1
  63. package/lib/message/group/C2C_MSG_REJECT.d.ts +9 -3
  64. package/lib/message/group/C2C_MSG_REJECT.js +1 -1
  65. package/lib/message/group/FRIEND_ADD.d.ts +13 -3
  66. package/lib/message/group/FRIEND_ADD.js +1 -1
  67. package/lib/message/group/FRIEND_DEL.d.ts +9 -3
  68. package/lib/message/group/FRIEND_DEL.js +1 -1
  69. package/lib/message/group/GROUP_ADD_ROBOT.d.ts +23 -4
  70. package/lib/message/group/GROUP_ADD_ROBOT.js +1 -1
  71. package/lib/message/group/GROUP_AT_MESSAGE_CREATE.d.ts +38 -32
  72. package/lib/message/group/GROUP_AT_MESSAGE_CREATE.js +1 -1
  73. package/lib/message/group/GROUP_DEL_ROBOT.d.ts +19 -4
  74. package/lib/message/group/GROUP_DEL_ROBOT.js +1 -1
  75. package/lib/message/group/GROUP_JOIN_REQUEST.d.ts +83 -22
  76. package/lib/message/group/GROUP_JOIN_REQUEST.js +1 -1
  77. package/lib/message/group/GROUP_MEMBER_ADD.d.ts +8 -5
  78. package/lib/message/group/GROUP_MEMBER_ADD.js +1 -1
  79. package/lib/message/group/GROUP_MEMBER_REMOVE.d.ts +8 -5
  80. package/lib/message/group/GROUP_MEMBER_REMOVE.js +1 -1
  81. package/lib/message/group/GROUP_MESSAGE_CREATE.d.ts +38 -32
  82. package/lib/message/group/GROUP_MESSAGE_CREATE.js +1 -1
  83. package/lib/message/group/GROUP_MSG_RECEIVE.d.ts +14 -4
  84. package/lib/message/group/GROUP_MSG_RECEIVE.js +1 -1
  85. package/lib/message/group/GROUP_MSG_REJECT.d.ts +10 -4
  86. package/lib/message/group/GROUP_MSG_REJECT.js +1 -1
  87. package/lib/message/group/MESSAGE_AUDIT_PASS.d.ts +12 -6
  88. package/lib/message/group/MESSAGE_AUDIT_PASS.js +1 -1
  89. package/lib/message/group/MESSAGE_AUDIT_REJECT.d.ts +12 -6
  90. package/lib/message/group/MESSAGE_AUDIT_REJECT.js +1 -1
  91. package/lib/register.d.ts +12 -9
  92. package/lib/register.js +1246 -1404
  93. package/lib/sdk/api.d.ts +937 -306
  94. package/lib/sdk/api.js +1481 -893
  95. package/lib/sdk/client.webhook.d.ts +25 -8
  96. package/lib/sdk/client.webhook.js +185 -189
  97. package/lib/sdk/client.websoket.d.ts +31 -9
  98. package/lib/sdk/client.websoket.js +369 -392
  99. package/lib/sdk/config.d.ts +6 -2
  100. package/lib/sdk/config.js +9 -6
  101. package/lib/sdk/instance.d.ts +11 -3
  102. package/lib/sdk/instance.js +72 -84
  103. package/lib/sdk/intents.d.ts +89 -3
  104. package/lib/sdk/intents.js +116 -33
  105. package/lib/sdk/message.d.ts +7 -4
  106. package/lib/sdk/message.group.d.ts +38 -35
  107. package/lib/sdk/message.group.js +1 -1
  108. package/lib/sdk/message.guild.d.ts +40 -37
  109. package/lib/sdk/message.guild.js +1 -1
  110. package/lib/sdk/message.js +1 -1
  111. package/lib/sdk/message.public.d.ts +8 -5
  112. package/lib/sdk/message.public.js +1 -1
  113. package/lib/sdk/qr-auth.d.ts +55 -0
  114. package/lib/sdk/qr-auth.js +166 -0
  115. package/lib/sdk/registry.d.ts +34 -24
  116. package/lib/sdk/registry.js +134 -114
  117. package/lib/sdk/session.d.ts +19 -15
  118. package/lib/sdk/session.js +51 -54
  119. package/lib/sdk/typing.d.ts +410 -184
  120. package/lib/sdk/typing.js +1 -1
  121. package/lib/sdk/webhook.secret.d.ts +33 -13
  122. package/lib/sdk/webhook.secret.js +50 -36
  123. package/lib/sends.d.ts +42 -25
  124. package/lib/sends.js +519 -541
  125. package/lib/types.d.ts +52 -41
  126. package/lib/types.js +1 -1
  127. package/lib/upload.d.ts +47 -18
  128. package/lib/upload.js +80 -51
  129. package/lib/utils.d.ts +9 -1
  130. package/lib/utils.js +10 -5
  131. package/package.json +10 -7
  132. package/LICENSE +0 -21
@@ -1,396 +1,373 @@
1
- import WebSocket from 'ws';
2
- import { QQBotAPI } from './api.js';
3
- import { getIntentsMask } from './intents.js';
4
- import { FileSessionStore } from './session.js';
1
+ import { QQBotAPI } from "./api.js";
2
+ import { getIntentsMask } from "./intents.js";
3
+ import { FileSessionStore } from "./session.js";
4
+ import WebSocket$1 from "ws";
5
5
 
6
+ //#region src/sdk/client.websoket.ts
6
7
  const normalizeGatewayMessage = (message) => {
7
- if (!message?.id || !message?.d || typeof message.d !== 'object' || Array.isArray(message.d)) {
8
- return message;
9
- }
10
- if (message.d.id === undefined || message.d.id === null || message.d.id === '') {
11
- message.d.id = message.id;
12
- }
13
- return message;
8
+ if (!message?.id || !message?.d || typeof message.d !== "object" || Array.isArray(message.d)) return message;
9
+ if (message.d.id === void 0 || message.d.id === null || message.d.id === "") message.d.id = message.id;
10
+ return message;
11
+ };
12
+ /**
13
+ * QQ 机器人网关连接。
14
+ *
15
+ * 网关会主动发送 op=7 要求客户端重新连接,也可能因网络或重复连接被关闭。
16
+ * 因此重连必须由客户端统一调度,避免旧 socket 的 close 事件重复创建连接。
17
+ */
18
+ var QQBotClients = class extends QQBotAPI {
19
+ #isConnected = false;
20
+ #stopped = false;
21
+ #sessionId = null;
22
+ #sessionStore;
23
+ #sessionLoaded = false;
24
+ #sequence = null;
25
+ #heartbeatAcknowledged = true;
26
+ #heartbeatInterval = 3e4;
27
+ #heartbeatTimer = null;
28
+ #accessTokenTimer = null;
29
+ #accessTokenTask = null;
30
+ #reconnectTimer = null;
31
+ #reconnectAttempts = 0;
32
+ #gatewayUrl = null;
33
+ #ws = null;
34
+ #events = {};
35
+ /**
36
+ * 设置配置
37
+ * @param option
38
+ */
39
+ constructor(option) {
40
+ super(option);
41
+ this.#sessionStore = option.sessionStore || new FileSessionStore();
42
+ }
43
+ async #loadSession() {
44
+ if (this.#sessionLoaded) return;
45
+ this.#sessionLoaded = true;
46
+ const botId = String(this.config.get("app_id") || "");
47
+ const session = botId ? await this.#sessionStore.load(botId) : null;
48
+ if (session) {
49
+ this.#sessionId = session.sessionId;
50
+ this.#sequence = session.sequence;
51
+ }
52
+ }
53
+ #persistSession() {
54
+ const botId = String(this.config.get("app_id") || "");
55
+ if (!botId || !this.#sessionId || this.#sequence === null) return;
56
+ this.#sessionStore.save({
57
+ botId,
58
+ sessionId: this.#sessionId,
59
+ sequence: this.#sequence
60
+ }).catch((err) => {
61
+ console.warn("[ws-qqbot] session persistence failed", err);
62
+ });
63
+ }
64
+ #clearSession() {
65
+ const botId = String(this.config.get("app_id") || "");
66
+ this.#sessionId = null;
67
+ this.#sequence = null;
68
+ if (botId) this.#sessionStore.clear(botId).catch(() => void 0);
69
+ }
70
+ #clearHeartbeat() {
71
+ if (this.#heartbeatTimer) {
72
+ clearInterval(this.#heartbeatTimer);
73
+ this.#heartbeatTimer = null;
74
+ }
75
+ }
76
+ #startHeartbeat(ws) {
77
+ this.#clearHeartbeat();
78
+ this.#heartbeatTimer = setInterval(() => {
79
+ if (this.#ws !== ws || !this.#isConnected || ws.readyState !== WebSocket$1.OPEN) {
80
+ this.#clearHeartbeat();
81
+ return;
82
+ }
83
+ if (!this.#heartbeatAcknowledged) {
84
+ this.#requestReconnect("heartbeat acknowledgement timeout");
85
+ return;
86
+ }
87
+ try {
88
+ this.#heartbeatAcknowledged = false;
89
+ this.updateConnectionStatus({ heartbeatAcknowledged: false });
90
+ ws.send(JSON.stringify({
91
+ op: 1,
92
+ d: null
93
+ }));
94
+ } catch (err) {
95
+ console.error("[ws-qqbot] heartbeat failed", err);
96
+ this.#requestReconnect("heartbeat failed");
97
+ }
98
+ }, this.#heartbeatInterval);
99
+ }
100
+ #getReconnectDelay() {
101
+ const delay = Math.min(1e3 * 2 ** Math.min(this.#reconnectAttempts, 5), 3e4);
102
+ return delay + Math.floor(Math.random() * Math.min(1e3, Math.floor(delay / 4)));
103
+ }
104
+ #scheduleReconnect(reason) {
105
+ if (this.#stopped || this.#reconnectTimer) return;
106
+ const delay = this.#getReconnectDelay();
107
+ this.#reconnectAttempts++;
108
+ this.updateConnectionStatus({
109
+ state: "reconnecting",
110
+ reconnectAttempts: this.#reconnectAttempts,
111
+ heartbeatAcknowledged: this.#heartbeatAcknowledged,
112
+ lastError: reason
113
+ });
114
+ console.info(`[ws-qqbot] ${reason}; reconnecting in ${delay}ms (attempt ${this.#reconnectAttempts})`);
115
+ this.#reconnectTimer = setTimeout(() => {
116
+ this.#reconnectTimer = null;
117
+ this.connect();
118
+ }, delay);
119
+ }
120
+ #requestReconnect(reason) {
121
+ this.#isConnected = false;
122
+ this.#clearHeartbeat();
123
+ this.cancelStreams();
124
+ this.#scheduleReconnect(reason);
125
+ const ws = this.#ws;
126
+ if (ws && ws.readyState !== WebSocket$1.CLOSED && ws.readyState !== WebSocket$1.CLOSING) ws.close();
127
+ }
128
+ /** Stops reconnecting and releases the active gateway socket. */
129
+ disconnect() {
130
+ this.#stopped = true;
131
+ this.#isConnected = false;
132
+ this.#clearHeartbeat();
133
+ if (this.#reconnectTimer) clearTimeout(this.#reconnectTimer);
134
+ if (this.#accessTokenTimer) clearTimeout(this.#accessTokenTimer);
135
+ this.#reconnectTimer = null;
136
+ this.#accessTokenTimer = null;
137
+ const ws = this.#ws;
138
+ this.#ws = null;
139
+ this.cancelStreams();
140
+ if (ws && ws.readyState !== WebSocket$1.CLOSED) ws.close(1e3, "client stopped");
141
+ this.updateConnectionStatus({
142
+ state: "stopped",
143
+ heartbeatAcknowledged: false
144
+ });
145
+ }
146
+ #scheduleAccessTokenRefresh(delay) {
147
+ if (this.#accessTokenTimer) clearTimeout(this.#accessTokenTimer);
148
+ this.#accessTokenTimer = setTimeout(() => {
149
+ this.#accessTokenTimer = null;
150
+ this.#startAccessTokenRefresh().catch(() => void 0);
151
+ }, delay);
152
+ }
153
+ async #refreshAccessToken() {
154
+ const appId = this.config.get("app_id");
155
+ const secret = this.config.get("secret");
156
+ if (!appId || !secret) throw new Error("QQ Bot app_id or secret is missing");
157
+ try {
158
+ const data = await this.getAuthentication();
159
+ if (!data?.access_token) throw new Error("QQ Bot access token is empty");
160
+ this.config.set("access_token", data.access_token);
161
+ const expiresIn = Number(data.expires_in);
162
+ const refreshIn = Math.max(6e4, (Number.isFinite(expiresIn) ? expiresIn * 1e3 : 36e5) - 6e4);
163
+ this.#scheduleAccessTokenRefresh(refreshIn);
164
+ console.info(`[ws-qqbot] access token refreshed; next refresh in ${refreshIn}ms`);
165
+ } catch (err) {
166
+ this.#scheduleAccessTokenRefresh(3e4);
167
+ console.error("[ws-qqbot] access token refresh failed; retrying in 30000ms", err);
168
+ throw err;
169
+ }
170
+ }
171
+ #startAccessTokenRefresh() {
172
+ if (this.#accessTokenTask === null) this.#accessTokenTask = this.#refreshAccessToken().finally(() => {
173
+ this.#accessTokenTask = null;
174
+ });
175
+ return this.#accessTokenTask;
176
+ }
177
+ async #ensureAccessToken() {
178
+ if (this.#accessTokenTimer) return;
179
+ await this.#startAccessTokenRefresh();
180
+ }
181
+ /**
182
+ * 鉴权数据
183
+ * @returns WebSocket Identify 数据
184
+ */
185
+ #aut() {
186
+ const token = this.config.get("access_token");
187
+ const intents = this.config.get("intents");
188
+ const shard = this.config.get("shard");
189
+ return {
190
+ op: 2,
191
+ d: {
192
+ token: `QQBot ${token}`,
193
+ intents: getIntentsMask(intents),
194
+ shard,
195
+ properties: {
196
+ $os: process.platform,
197
+ $browser: "alemonjs",
198
+ $device: "alemonjs"
199
+ }
200
+ }
201
+ };
202
+ }
203
+ #resume() {
204
+ return {
205
+ op: 6,
206
+ d: {
207
+ token: `QQBot ${this.config.get("access_token")}`,
208
+ session_id: this.#sessionId,
209
+ seq: this.#sequence
210
+ }
211
+ };
212
+ }
213
+ /**
214
+ * 注册事件处理程序
215
+ * @param key 事件名称
216
+ * @param val 事件处理函数
217
+ */
218
+ on(key, val) {
219
+ if (!this.#events[key]) this.#events[key] = [];
220
+ this.#events[key].push(val);
221
+ return this;
222
+ }
223
+ #emitError(err) {
224
+ for (const event of this.#events.ERROR || []) try {
225
+ event(err);
226
+ } catch (error) {
227
+ console.error("[ws-qqbot] error handler failed", error);
228
+ }
229
+ }
230
+ #handleDispatch(t, d) {
231
+ for (const event of this.#events[t] || []) try {
232
+ event(d);
233
+ } catch (err) {
234
+ this.#emitError(err);
235
+ }
236
+ }
237
+ /**
238
+ * 建立或恢复网关连接。
239
+ */
240
+ async connect(gatewayURL) {
241
+ this.#stopped = false;
242
+ if (gatewayURL) this.#gatewayUrl = gatewayURL;
243
+ if (this.#ws && this.#ws.readyState !== WebSocket$1.CLOSED) return;
244
+ await this.#loadSession();
245
+ try {
246
+ await this.#ensureAccessToken();
247
+ } catch {
248
+ this.#scheduleReconnect("access token unavailable");
249
+ return;
250
+ }
251
+ if (!this.#gatewayUrl) try {
252
+ this.#gatewayUrl = await this.gateway().then((res) => res?.url);
253
+ } catch (err) {
254
+ this.#emitError(err);
255
+ this.#scheduleReconnect("gateway request failed");
256
+ return;
257
+ }
258
+ if (!this.#gatewayUrl) {
259
+ this.#scheduleReconnect("gateway URL is empty");
260
+ return;
261
+ }
262
+ let ws;
263
+ try {
264
+ ws = new WebSocket$1(this.#gatewayUrl);
265
+ } catch (err) {
266
+ this.#emitError(err);
267
+ this.#scheduleReconnect("gateway connection creation failed");
268
+ return;
269
+ }
270
+ this.#ws = ws;
271
+ this.updateConnectionStatus({
272
+ state: "connecting",
273
+ transport: "websocket",
274
+ lastError: void 0
275
+ });
276
+ ws.on("open", () => {
277
+ if (this.#ws === ws) console.info("[ws-qqbot] open");
278
+ });
279
+ ws.on("message", (msg) => {
280
+ if (this.#ws !== ws) return;
281
+ try {
282
+ const message = normalizeGatewayMessage(JSON.parse(msg.toString("utf8")));
283
+ if (process.env.NTQQ_WS === "dev") console.info("message", message);
284
+ const map = {
285
+ 0: ({ t, d, s }) => {
286
+ if (typeof s === "number") this.#sequence = s;
287
+ if (t === "READY" && d?.session_id) this.#sessionId = String(d.session_id);
288
+ this.#persistSession();
289
+ this.#handleDispatch(t, d);
290
+ if (t === "READY" || t === "RESUMED") {
291
+ this.#reconnectAttempts = 0;
292
+ this.updateConnectionStatus({
293
+ state: "ready",
294
+ reconnectAttempts: 0,
295
+ heartbeatAcknowledged: this.#heartbeatAcknowledged,
296
+ sessionId: this.#sessionId ?? void 0,
297
+ sequence: this.#sequence ?? void 0,
298
+ resumed: t === "RESUMED"
299
+ });
300
+ console.info(t === "READY" ? "[ws-qqbot] ready" : "[ws-qqbot] restored connection");
301
+ }
302
+ },
303
+ 7: ({ d }) => {
304
+ console.info("[ws-qqbot] gateway requested reconnect", d);
305
+ this.#requestReconnect("gateway requested reconnect");
306
+ },
307
+ 9: ({ d }) => {
308
+ console.error("[ws-qqbot] invalid session", d);
309
+ this.#clearSession();
310
+ this.#requestReconnect("invalid session");
311
+ },
312
+ 10: ({ d }) => {
313
+ this.#isConnected = true;
314
+ this.#heartbeatInterval = Number(d?.heartbeat_interval) || 3e4;
315
+ this.#startHeartbeat(ws);
316
+ try {
317
+ const resumable = this.#sessionId && this.#sequence !== null;
318
+ ws.send(JSON.stringify(resumable ? this.#resume() : this.#aut()));
319
+ this.updateConnectionStatus({
320
+ state: "connecting",
321
+ heartbeatAcknowledged: true,
322
+ sessionId: this.#sessionId ?? void 0,
323
+ sequence: this.#sequence ?? void 0,
324
+ resumed: Boolean(resumable)
325
+ });
326
+ } catch (err) {
327
+ console.error("[ws-qqbot] identify failed", err);
328
+ this.#emitError(err);
329
+ this.#requestReconnect("identify failed");
330
+ }
331
+ },
332
+ 11: () => {
333
+ this.#heartbeatAcknowledged = true;
334
+ this.updateConnectionStatus({ heartbeatAcknowledged: true });
335
+ console.debug("[ws-qqbot] heartbeat acknowledged");
336
+ },
337
+ 12: ({ d }) => {
338
+ console.debug("[ws-qqbot] platform data", d);
339
+ }
340
+ };
341
+ if (map[message.op]) map[message.op](message);
342
+ } catch (err) {
343
+ console.error("[ws-qqbot] invalid gateway message", err);
344
+ this.#emitError(err);
345
+ }
346
+ });
347
+ ws.on("close", (code, reason) => {
348
+ if (this.#ws !== ws) return;
349
+ this.#ws = null;
350
+ this.#isConnected = false;
351
+ this.#clearHeartbeat();
352
+ this.updateConnectionStatus({
353
+ state: "offline",
354
+ heartbeatAcknowledged: false
355
+ });
356
+ console.info(`[ws-qqbot] close ${code}${reason.length ? `: ${reason.toString("utf8")}` : ""}`);
357
+ this.#scheduleReconnect("connection closed");
358
+ });
359
+ ws.on("error", (err) => {
360
+ if (this.#ws !== ws) return;
361
+ console.error("[ws-qqbot] error", err);
362
+ this.updateConnectionStatus({
363
+ state: "offline",
364
+ lastError: err.message
365
+ });
366
+ this.#emitError(err);
367
+ this.#requestReconnect("connection error");
368
+ });
369
+ }
14
370
  };
15
- class QQBotClients extends QQBotAPI {
16
- #isConnected = false;
17
- #stopped = false;
18
- #sessionId = null;
19
- #sessionStore;
20
- #sessionLoaded = false;
21
- #sequence = null;
22
- #heartbeatAcknowledged = true;
23
- #heartbeatInterval = 30000;
24
- #heartbeatTimer = null;
25
- #accessTokenTimer = null;
26
- #accessTokenTask = null;
27
- #reconnectTimer = null;
28
- #reconnectAttempts = 0;
29
- #gatewayUrl = null;
30
- #ws = null;
31
- #events = {};
32
- constructor(option) {
33
- super(option);
34
- this.#sessionStore = option.sessionStore || new FileSessionStore();
35
- }
36
- async #loadSession() {
37
- if (this.#sessionLoaded)
38
- return;
39
- this.#sessionLoaded = true;
40
- const botId = String(this.config.get('app_id') || '');
41
- const session = botId ? await this.#sessionStore.load(botId) : null;
42
- if (session) {
43
- this.#sessionId = session.sessionId;
44
- this.#sequence = session.sequence;
45
- }
46
- }
47
- #persistSession() {
48
- const botId = String(this.config.get('app_id') || '');
49
- if (!botId || !this.#sessionId || this.#sequence === null)
50
- return;
51
- void this.#sessionStore.save({ botId, sessionId: this.#sessionId, sequence: this.#sequence }).catch(err => {
52
- console.warn('[ws-qqbot] session persistence failed', err);
53
- });
54
- }
55
- #clearSession() {
56
- const botId = String(this.config.get('app_id') || '');
57
- this.#sessionId = null;
58
- this.#sequence = null;
59
- if (botId)
60
- void this.#sessionStore.clear(botId).catch(() => undefined);
61
- }
62
- #clearHeartbeat() {
63
- if (this.#heartbeatTimer) {
64
- clearInterval(this.#heartbeatTimer);
65
- this.#heartbeatTimer = null;
66
- }
67
- }
68
- #startHeartbeat(ws) {
69
- this.#clearHeartbeat();
70
- this.#heartbeatTimer = setInterval(() => {
71
- if (this.#ws !== ws || !this.#isConnected || ws.readyState !== WebSocket.OPEN) {
72
- this.#clearHeartbeat();
73
- return;
74
- }
75
- if (!this.#heartbeatAcknowledged) {
76
- this.#requestReconnect('heartbeat acknowledgement timeout');
77
- return;
78
- }
79
- try {
80
- this.#heartbeatAcknowledged = false;
81
- this.updateConnectionStatus({ heartbeatAcknowledged: false });
82
- ws.send(JSON.stringify({
83
- op: 1,
84
- d: null
85
- }));
86
- }
87
- catch (err) {
88
- console.error('[ws-qqbot] heartbeat failed', err);
89
- this.#requestReconnect('heartbeat failed');
90
- }
91
- }, this.#heartbeatInterval);
92
- }
93
- #getReconnectDelay() {
94
- const delay = Math.min(1000 * 2 ** Math.min(this.#reconnectAttempts, 5), 30000);
95
- const jitter = Math.floor(Math.random() * Math.min(1000, Math.floor(delay / 4)));
96
- return delay + jitter;
97
- }
98
- #scheduleReconnect(reason) {
99
- if (this.#stopped || this.#reconnectTimer) {
100
- return;
101
- }
102
- const delay = this.#getReconnectDelay();
103
- this.#reconnectAttempts++;
104
- this.updateConnectionStatus({
105
- state: 'reconnecting',
106
- reconnectAttempts: this.#reconnectAttempts,
107
- heartbeatAcknowledged: this.#heartbeatAcknowledged,
108
- lastError: reason
109
- });
110
- console.info(`[ws-qqbot] ${reason}; reconnecting in ${delay}ms (attempt ${this.#reconnectAttempts})`);
111
- this.#reconnectTimer = setTimeout(() => {
112
- this.#reconnectTimer = null;
113
- void this.connect();
114
- }, delay);
115
- }
116
- #requestReconnect(reason) {
117
- this.#isConnected = false;
118
- this.#clearHeartbeat();
119
- this.cancelStreams();
120
- this.#scheduleReconnect(reason);
121
- const ws = this.#ws;
122
- if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
123
- ws.close();
124
- }
125
- }
126
- disconnect() {
127
- this.#stopped = true;
128
- this.#isConnected = false;
129
- this.#clearHeartbeat();
130
- if (this.#reconnectTimer) {
131
- clearTimeout(this.#reconnectTimer);
132
- }
133
- if (this.#accessTokenTimer) {
134
- clearTimeout(this.#accessTokenTimer);
135
- }
136
- this.#reconnectTimer = null;
137
- this.#accessTokenTimer = null;
138
- const ws = this.#ws;
139
- this.#ws = null;
140
- this.cancelStreams();
141
- if (ws && ws.readyState !== WebSocket.CLOSED) {
142
- ws.close(1000, 'client stopped');
143
- }
144
- this.updateConnectionStatus({ state: 'stopped', heartbeatAcknowledged: false });
145
- }
146
- #scheduleAccessTokenRefresh(delay) {
147
- if (this.#accessTokenTimer) {
148
- clearTimeout(this.#accessTokenTimer);
149
- }
150
- this.#accessTokenTimer = setTimeout(() => {
151
- this.#accessTokenTimer = null;
152
- void this.#startAccessTokenRefresh().catch(() => undefined);
153
- }, delay);
154
- }
155
- async #refreshAccessToken() {
156
- const appId = this.config.get('app_id');
157
- const secret = this.config.get('secret');
158
- if (!appId || !secret) {
159
- throw new Error('QQ Bot app_id or secret is missing');
160
- }
161
- try {
162
- const data = await this.getAuthentication();
163
- if (!data?.access_token) {
164
- throw new Error('QQ Bot access token is empty');
165
- }
166
- this.config.set('access_token', data.access_token);
167
- const expiresIn = Number(data.expires_in);
168
- const refreshIn = Math.max(60000, (Number.isFinite(expiresIn) ? expiresIn * 1000 : 3600000) - 60000);
169
- this.#scheduleAccessTokenRefresh(refreshIn);
170
- console.info(`[ws-qqbot] access token refreshed; next refresh in ${refreshIn}ms`);
171
- }
172
- catch (err) {
173
- this.#scheduleAccessTokenRefresh(30000);
174
- console.error('[ws-qqbot] access token refresh failed; retrying in 30000ms', err);
175
- throw err;
176
- }
177
- }
178
- #startAccessTokenRefresh() {
179
- if (this.#accessTokenTask === null) {
180
- this.#accessTokenTask = this.#refreshAccessToken().finally(() => {
181
- this.#accessTokenTask = null;
182
- });
183
- }
184
- return this.#accessTokenTask;
185
- }
186
- async #ensureAccessToken() {
187
- if (this.#accessTokenTimer) {
188
- return;
189
- }
190
- await this.#startAccessTokenRefresh();
191
- }
192
- #aut() {
193
- const token = this.config.get('access_token');
194
- const intents = this.config.get('intents');
195
- const shard = this.config.get('shard');
196
- return {
197
- op: 2,
198
- d: {
199
- token: `QQBot ${token}`,
200
- intents: getIntentsMask(intents),
201
- shard,
202
- properties: {
203
- $os: process.platform,
204
- $browser: 'alemonjs',
205
- $device: 'alemonjs'
206
- }
207
- }
208
- };
209
- }
210
- #resume() {
211
- return {
212
- op: 6,
213
- d: {
214
- token: `QQBot ${this.config.get('access_token')}`,
215
- session_id: this.#sessionId,
216
- seq: this.#sequence
217
- }
218
- };
219
- }
220
- on(key, val) {
221
- if (!this.#events[key]) {
222
- this.#events[key] = [];
223
- }
224
- this.#events[key].push(val);
225
- return this;
226
- }
227
- #emitError(err) {
228
- for (const event of this.#events.ERROR || []) {
229
- try {
230
- event(err);
231
- }
232
- catch (error) {
233
- console.error('[ws-qqbot] error handler failed', error);
234
- }
235
- }
236
- }
237
- #handleDispatch(t, d) {
238
- for (const event of this.#events[t] || []) {
239
- try {
240
- event(d);
241
- }
242
- catch (err) {
243
- this.#emitError(err);
244
- }
245
- }
246
- }
247
- async connect(gatewayURL) {
248
- this.#stopped = false;
249
- if (gatewayURL) {
250
- this.#gatewayUrl = gatewayURL;
251
- }
252
- if (this.#ws && this.#ws.readyState !== WebSocket.CLOSED) {
253
- return;
254
- }
255
- await this.#loadSession();
256
- try {
257
- await this.#ensureAccessToken();
258
- }
259
- catch {
260
- this.#scheduleReconnect('access token unavailable');
261
- return;
262
- }
263
- if (!this.#gatewayUrl) {
264
- try {
265
- this.#gatewayUrl = await this.gateway().then(res => res?.url);
266
- }
267
- catch (err) {
268
- this.#emitError(err);
269
- this.#scheduleReconnect('gateway request failed');
270
- return;
271
- }
272
- }
273
- if (!this.#gatewayUrl) {
274
- this.#scheduleReconnect('gateway URL is empty');
275
- return;
276
- }
277
- let ws;
278
- try {
279
- ws = new WebSocket(this.#gatewayUrl);
280
- }
281
- catch (err) {
282
- this.#emitError(err);
283
- this.#scheduleReconnect('gateway connection creation failed');
284
- return;
285
- }
286
- this.#ws = ws;
287
- this.updateConnectionStatus({ state: 'connecting', transport: 'websocket', lastError: undefined });
288
- ws.on('open', () => {
289
- if (this.#ws === ws) {
290
- console.info('[ws-qqbot] open');
291
- }
292
- });
293
- ws.on('message', msg => {
294
- if (this.#ws !== ws) {
295
- return;
296
- }
297
- try {
298
- const message = normalizeGatewayMessage(JSON.parse(msg.toString('utf8')));
299
- if (process.env.NTQQ_WS === 'dev') {
300
- console.info('message', message);
301
- }
302
- const map = {
303
- 0: ({ t, d, s }) => {
304
- if (typeof s === 'number') {
305
- this.#sequence = s;
306
- }
307
- if (t === 'READY' && d?.session_id) {
308
- this.#sessionId = String(d.session_id);
309
- }
310
- this.#persistSession();
311
- this.#handleDispatch(t, d);
312
- if (t === 'READY' || t === 'RESUMED') {
313
- this.#reconnectAttempts = 0;
314
- this.updateConnectionStatus({
315
- state: 'ready',
316
- reconnectAttempts: 0,
317
- heartbeatAcknowledged: this.#heartbeatAcknowledged,
318
- sessionId: this.#sessionId ?? undefined,
319
- sequence: this.#sequence ?? undefined,
320
- resumed: t === 'RESUMED'
321
- });
322
- console.info(t === 'READY' ? '[ws-qqbot] ready' : '[ws-qqbot] restored connection');
323
- }
324
- },
325
- 7: ({ d }) => {
326
- console.info('[ws-qqbot] gateway requested reconnect', d);
327
- this.#requestReconnect('gateway requested reconnect');
328
- },
329
- 9: ({ d }) => {
330
- console.error('[ws-qqbot] invalid session', d);
331
- this.#clearSession();
332
- this.#requestReconnect('invalid session');
333
- },
334
- 10: ({ d }) => {
335
- this.#isConnected = true;
336
- this.#heartbeatInterval = Number(d?.heartbeat_interval) || 30000;
337
- this.#startHeartbeat(ws);
338
- try {
339
- const resumable = this.#sessionId && this.#sequence !== null;
340
- ws.send(JSON.stringify(resumable ? this.#resume() : this.#aut()));
341
- this.updateConnectionStatus({
342
- state: 'connecting',
343
- heartbeatAcknowledged: true,
344
- sessionId: this.#sessionId ?? undefined,
345
- sequence: this.#sequence ?? undefined,
346
- resumed: Boolean(resumable)
347
- });
348
- }
349
- catch (err) {
350
- console.error('[ws-qqbot] identify failed', err);
351
- this.#emitError(err);
352
- this.#requestReconnect('identify failed');
353
- }
354
- },
355
- 11: () => {
356
- this.#heartbeatAcknowledged = true;
357
- this.updateConnectionStatus({ heartbeatAcknowledged: true });
358
- console.debug('[ws-qqbot] heartbeat acknowledged');
359
- },
360
- 12: ({ d }) => {
361
- console.debug('[ws-qqbot] platform data', d);
362
- }
363
- };
364
- if (map[message.op]) {
365
- map[message.op](message);
366
- }
367
- }
368
- catch (err) {
369
- console.error('[ws-qqbot] invalid gateway message', err);
370
- this.#emitError(err);
371
- }
372
- });
373
- ws.on('close', (code, reason) => {
374
- if (this.#ws !== ws) {
375
- return;
376
- }
377
- this.#ws = null;
378
- this.#isConnected = false;
379
- this.#clearHeartbeat();
380
- this.updateConnectionStatus({ state: 'offline', heartbeatAcknowledged: false });
381
- console.info(`[ws-qqbot] close ${code}${reason.length ? `: ${reason.toString('utf8')}` : ''}`);
382
- this.#scheduleReconnect('connection closed');
383
- });
384
- ws.on('error', err => {
385
- if (this.#ws !== ws) {
386
- return;
387
- }
388
- console.error('[ws-qqbot] error', err);
389
- this.updateConnectionStatus({ state: 'offline', lastError: err.message });
390
- this.#emitError(err);
391
- this.#requestReconnect('connection error');
392
- });
393
- }
394
- }
395
371
 
396
- export { QQBotClients };
372
+ //#endregion
373
+ export { QQBotClients };