@alemonjs/qq-bot 2.1.30 → 2.1.32

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.
@@ -1,8 +1,7 @@
1
1
  import WebSocket from 'ws';
2
2
  import { QQBotAPI } from './api.js';
3
- import { config } from './config.js';
4
3
  import { getIntentsMask } from './intents.js';
5
- import { Counter } from 'alemonjs/utils';
4
+ import { FileSessionStore } from './session.js';
6
5
 
7
6
  const normalizeGatewayMessage = (message) => {
8
7
  if (!message?.id || !message?.d || typeof message.d !== 'object' || Array.isArray(message.d)) {
@@ -14,41 +13,192 @@ const normalizeGatewayMessage = (message) => {
14
13
  return message;
15
14
  };
16
15
  class QQBotClients extends QQBotAPI {
17
- #counter = new Counter(1);
18
16
  #isConnected = false;
19
- #heartbeat_interval = 30000;
20
- #IntervalId = null;
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;
21
29
  #gatewayUrl = null;
22
- constructor(opstion) {
23
- super();
24
- for (const key in opstion) {
25
- config.set(key, opstion[key]);
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;
26
66
  }
27
67
  }
28
- async #setTimeoutBotConfig() {
29
- const accessToken = async () => {
30
- const app_id = config.get('app_id');
31
- const secret = config.get('secret');
32
- if (!app_id || !secret) {
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');
33
77
  return;
34
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 {
35
162
  const data = await this.getAuthentication();
36
- config.set('access_token', data.access_token);
37
- console.info('refresh', data.expires_in, 's');
38
- setTimeout(() => void accessToken(), data.expires_in * 1000);
39
- };
40
- await accessToken();
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();
41
191
  }
42
192
  #aut() {
43
- const token = config.get('access_token');
44
- const intents = config.get('intents');
45
- const shard = config.get('shard');
193
+ const token = this.config.get('access_token');
194
+ const intents = this.config.get('intents');
195
+ const shard = this.config.get('shard');
46
196
  return {
47
197
  op: 2,
48
198
  d: {
49
199
  token: `QQBot ${token}`,
50
200
  intents: getIntentsMask(intents),
51
- shard: shard,
201
+ shard,
52
202
  properties: {
53
203
  $os: process.platform,
54
204
  $browser: 'alemonjs',
@@ -57,8 +207,16 @@ class QQBotClients extends QQBotAPI {
57
207
  }
58
208
  };
59
209
  }
60
- #ws = null;
61
- #events = {};
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
+ }
62
220
  on(key, val) {
63
221
  if (!this.#events[key]) {
64
222
  this.#events[key] = [];
@@ -66,119 +224,172 @@ class QQBotClients extends QQBotAPI {
66
224
  this.#events[key].push(val);
67
225
  return this;
68
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
+ }
69
247
  async connect(gatewayURL) {
70
- await this.#setTimeoutBotConfig();
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
+ }
71
263
  if (!this.#gatewayUrl) {
72
- this.#gatewayUrl = gatewayURL ?? (await this.gateway().then(res => res?.url));
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
+ }
73
272
  }
74
273
  if (!this.#gatewayUrl) {
274
+ this.#scheduleReconnect('gateway URL is empty');
75
275
  return;
76
276
  }
77
- const reconnect = () => {
78
- if (this.#counter.value >= 5) {
79
- console.info('The maximum number of reconnections has been reached, cancel reconnection');
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) {
80
295
  return;
81
296
  }
82
- setTimeout(() => {
83
- console.info('[ws-qqbot] reconnecting...');
84
- start();
85
- this.#counter.next();
86
- }, 5000);
87
- };
88
- const start = () => {
89
- if (this.#gatewayUrl) {
297
+ try {
298
+ const message = normalizeGatewayMessage(JSON.parse(msg.toString('utf8')));
299
+ if (process.env.NTQQ_WS === 'dev') {
300
+ console.info('message', message);
301
+ }
90
302
  const map = {
91
- 0: ({ t, d }) => {
92
- if (this.#events[t]) {
93
- try {
94
- for (const event of this.#events[t]) {
95
- if (typeof event !== 'function') {
96
- continue;
97
- }
98
- event(d);
99
- }
100
- }
101
- catch (err) {
102
- if (this.#events['ERROR']) {
103
- for (const event of this.#events['ERROR']) {
104
- if (typeof event !== 'function') {
105
- continue;
106
- }
107
- event(err);
108
- }
109
- }
110
- }
303
+ 0: ({ t, d, s }) => {
304
+ if (typeof s === 'number') {
305
+ this.#sequence = s;
111
306
  }
112
- if (t === 'READY') {
113
- if (this.#IntervalId) {
114
- clearInterval(this.#IntervalId);
115
- }
116
- this.#IntervalId = setInterval(() => {
117
- if (this.#isConnected && this.#ws) {
118
- this.#ws.send(JSON.stringify({
119
- op: 1,
120
- d: null
121
- }));
122
- }
123
- }, this.#heartbeat_interval);
307
+ if (t === 'READY' && d?.session_id) {
308
+ this.#sessionId = String(d.session_id);
124
309
  }
125
- if (t === 'RESUMED') {
126
- console.info('[ws-qqbot] restore connection');
127
- this.#counter.reStart();
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');
128
323
  }
129
324
  },
130
- 6: ({ d }) => {
131
- console.info('[ws-qqbot] connection attempt', d);
132
- },
133
325
  7: ({ d }) => {
134
- console.info('[ws-qqbot] reconnect', d);
135
- if (this.#IntervalId) {
136
- clearInterval(this.#IntervalId);
137
- }
326
+ console.info('[ws-qqbot] gateway requested reconnect', d);
327
+ this.#requestReconnect('gateway requested reconnect');
138
328
  },
139
329
  9: ({ d }) => {
140
- console.info('[ws-qqbot] parameter error', d);
330
+ console.error('[ws-qqbot] invalid session', d);
331
+ this.#clearSession();
332
+ this.#requestReconnect('invalid session');
141
333
  },
142
334
  10: ({ d }) => {
143
335
  this.#isConnected = true;
144
- this.#heartbeat_interval = d.heartbeat_interval;
145
- if (this.#ws) {
146
- this.#ws.send(JSON.stringify(this.#aut()));
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');
147
353
  }
148
354
  },
149
355
  11: () => {
150
- console.info('[ws-qqbot] heartbeat transmission');
151
- this.#counter.reStart();
356
+ this.#heartbeatAcknowledged = true;
357
+ this.updateConnectionStatus({ heartbeatAcknowledged: true });
358
+ console.debug('[ws-qqbot] heartbeat acknowledged');
152
359
  },
153
360
  12: ({ d }) => {
154
361
  console.debug('[ws-qqbot] platform data', d);
155
362
  }
156
363
  };
157
- this.#ws = new WebSocket(this.#gatewayUrl);
158
- this.#ws.on('open', () => {
159
- console.info('[ws-qqbot] open');
160
- });
161
- this.#ws.on('message', msg => {
162
- const message = normalizeGatewayMessage(JSON.parse(msg.toString('utf8')));
163
- if (process.env.NTQQ_WS === 'dev') {
164
- console.info('message', message);
165
- }
166
- if (map[message.op]) {
167
- map[message.op](message);
168
- }
169
- });
170
- this.#ws.on('close', err => {
171
- this.#isConnected = false;
172
- if (this.#IntervalId) {
173
- clearInterval(this.#IntervalId);
174
- this.#IntervalId = null;
175
- }
176
- void reconnect();
177
- console.info('[ws-qqbot] close', err);
178
- });
364
+ if (map[message.op]) {
365
+ map[message.op](message);
366
+ }
179
367
  }
180
- };
181
- start();
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
+ });
182
393
  }
183
394
  }
184
395
 
@@ -1 +1,3 @@
1
- export declare const config: Map<string, any>;
1
+ export declare class QQBotConfig extends Map<string, any> {
2
+ constructor(value?: Record<string, unknown>);
3
+ }
package/lib/sdk/config.js CHANGED
@@ -1,3 +1,7 @@
1
- const config = new Map();
1
+ class QQBotConfig extends Map {
2
+ constructor(value = {}) {
3
+ super(Object.entries(value));
4
+ }
5
+ }
2
6
 
3
- export { config };
7
+ export { QQBotConfig };
@@ -0,0 +1,25 @@
1
+ import { cbpPlatform } from 'alemonjs';
2
+ import { QQBotClients } from './client.websoket.js';
3
+ import type { Options } from './typing.js';
4
+ export declare class QQBotRegistry {
5
+ #private;
6
+ constructor(defaultBot?: string, cbp?: ReturnType<typeof cbpPlatform>);
7
+ add(botId: string, options: Options): QQBotClients;
8
+ get(botId?: string): QQBotClients;
9
+ getConnectionStatus(botId?: string): {
10
+ Platform: string;
11
+ state: import("./api.js").QQBotConnectionState;
12
+ bots: {
13
+ state: import("./api.js").QQBotConnectionState;
14
+ transport: "websocket" | "webhook" | "proxy" | null;
15
+ reconnectAttempts: number;
16
+ heartbeatAcknowledged: boolean;
17
+ sessionId?: string;
18
+ sequence?: number;
19
+ resumed: boolean;
20
+ lastError?: string;
21
+ BotId: string;
22
+ }[];
23
+ };
24
+ disconnect(): void;
25
+ }
@@ -0,0 +1,116 @@
1
+ import { cbpPlatform, createResult, ResultCode } from 'alemonjs';
2
+ import { getQQBotConfig } from '../config.js';
3
+ import { register } from '../register.js';
4
+ import { QQBotClients } from './client.websoket.js';
5
+
6
+ class QQBotRegistry {
7
+ #bots = new Map();
8
+ #streams = new Map();
9
+ #defaultBot;
10
+ #cbp;
11
+ constructor(defaultBot, cbp) {
12
+ this.#defaultBot = defaultBot;
13
+ if (cbp) {
14
+ this.#cbp = cbp;
15
+ }
16
+ else {
17
+ const config = getQQBotConfig();
18
+ const port = process.env.port || config.port || 17117;
19
+ this.#cbp = cbpPlatform(`ws://127.0.0.1:${port}`);
20
+ }
21
+ this.#cbp.onactions((data, consume) => void this.#onAction(data, consume));
22
+ this.#cbp.onapis((data, consume) => void this.#onApi(data, consume));
23
+ }
24
+ add(botId, options) {
25
+ if (!botId)
26
+ throw new Error('QQ BotId is required');
27
+ if (this.#bots.has(botId))
28
+ throw new Error(`Duplicate QQ BotId: ${botId}`);
29
+ const client = new QQBotClients({ ...options, app_id: botId });
30
+ client.setStreamLifecycleListener(streamId => this.#streams.delete(streamId));
31
+ const adapter = register(client, { botId, cbp: this.#cbp, bindActions: false });
32
+ this.#bots.set(botId, { client, adapter });
33
+ return client;
34
+ }
35
+ get(botId) {
36
+ return botId ? this.#bots.get(botId)?.client : undefined;
37
+ }
38
+ getConnectionStatus(botId) {
39
+ const bots = [...this.#bots].flatMap(([BotId, entry]) => {
40
+ if (botId && botId !== BotId)
41
+ return [];
42
+ return [{ BotId, ...entry.client.getConnectionStatus() }];
43
+ });
44
+ const state = bots.some(item => item.state === 'ready')
45
+ ? 'ready'
46
+ : bots.some(item => item.state === 'connecting' || item.state === 'reconnecting')
47
+ ? 'connecting'
48
+ : bots[0]?.state || 'idle';
49
+ return { Platform: 'qq-bot', state, bots };
50
+ }
51
+ disconnect() {
52
+ for (const { client } of this.#bots.values())
53
+ client.disconnect();
54
+ this.#streams.clear();
55
+ }
56
+ #requestedBotId(data) {
57
+ const payload = data?.payload || {};
58
+ return payload.target?.BotId || payload.BotId || payload.event?.BotId || payload.event?.Platform?.BotId;
59
+ }
60
+ #select(data) {
61
+ const requested = this.#requestedBotId(data);
62
+ if (requested) {
63
+ const entry = this.#bots.get(String(requested));
64
+ return entry ? { entry } : { error: `BotId ${requested} is not configured (available: ${[...this.#bots.keys()].join(', ') || 'none'})` };
65
+ }
66
+ if (this.#bots.size === 1)
67
+ return { entry: this.#bots.values().next().value };
68
+ if (this.#defaultBot)
69
+ return { entry: this.#bots.get(this.#defaultBot) };
70
+ return { error: 'BotId is required because multiple QQ bots are active and no default_bot is configured' };
71
+ }
72
+ async #onAction(data, consume) {
73
+ if (data?.action === 'connection.status') {
74
+ const botId = data?.payload?.BotId;
75
+ const status = this.getConnectionStatus(botId);
76
+ if (botId && !status.bots.length) {
77
+ consume([createResult(ResultCode.FailParams, `BotId ${botId} is not configured`, null)]);
78
+ }
79
+ else {
80
+ consume([createResult(ResultCode.Ok, data.action, status)]);
81
+ }
82
+ return;
83
+ }
84
+ const { entry, error } = this.#select(data);
85
+ if (!entry) {
86
+ consume([createResult(ResultCode.FailParams, error || 'QQ Bot is unavailable', null)]);
87
+ return;
88
+ }
89
+ await entry.adapter.onAction(data, consume);
90
+ }
91
+ async #onApi(data, consume) {
92
+ const params = data?.payload?.params;
93
+ const key = String(data?.payload?.key || '');
94
+ const method = key.split('.').at(-1);
95
+ const streamId = method === 'streamUpdate' || method === 'streamComplete' || method === 'streamCancel' ? params?.[0] : undefined;
96
+ const requested = (Array.isArray(params) ? params[0]?.BotId : undefined) || (streamId ? this.#streams.get(String(streamId)) : undefined);
97
+ const { entry, error } = this.#select({ payload: { BotId: requested, event: data?.payload?.event } });
98
+ if (!entry) {
99
+ consume([createResult(ResultCode.FailParams, error || 'QQ Bot is unavailable', null)]);
100
+ return;
101
+ }
102
+ await entry.adapter.onApi(data, results => {
103
+ const result = results.find((item) => item.code === ResultCode.Ok)?.data;
104
+ if (method === 'streamOpen' && result?.streamId)
105
+ this.#streams.set(String(result.streamId), requested || this.#botIdFor(entry));
106
+ if (streamId && (method === 'streamComplete' || method === 'streamCancel'))
107
+ this.#streams.delete(String(streamId));
108
+ consume(results);
109
+ });
110
+ }
111
+ #botIdFor(entry) {
112
+ return [...this.#bots.entries()].find(([, value]) => value === entry)?.[0] || '';
113
+ }
114
+ }
115
+
116
+ export { QQBotRegistry };
@@ -0,0 +1,18 @@
1
+ export type PersistedSession = {
2
+ botId: string;
3
+ sessionId: string;
4
+ sequence: number;
5
+ };
6
+ export interface SessionStore {
7
+ load(botId: string): Promise<PersistedSession | null>;
8
+ save(session: PersistedSession): Promise<void>;
9
+ clear(botId: string): Promise<void>;
10
+ }
11
+ export declare class FileSessionStore implements SessionStore {
12
+ #private;
13
+ private readonly root;
14
+ constructor(root?: string);
15
+ load(botId: string): Promise<PersistedSession | null>;
16
+ save(session: PersistedSession): Promise<void>;
17
+ clear(botId: string): Promise<void>;
18
+ }