@zhin.js/adapter-qq 9.0.0 → 9.0.1
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 +11 -0
- package/lib/endpoint.d.ts +1 -2
- package/lib/endpoint.js +119 -65
- package/package.json +9 -9
- package/src/endpoint.ts +101 -65
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @zhin.js/adapter-qq
|
|
2
2
|
|
|
3
|
+
## 9.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f7ee28d: Use the shared Endpoint lifecycle for QQ WebSocket and HTTP receivers. Isolate each SDK instance and its Webhook routes, close resources created by late startup completion after cancellation, and ignore callbacks queued by replaced SDK instances. Failed startup releases resources before retrying.
|
|
8
|
+
- Updated dependencies [f7ee28d]
|
|
9
|
+
- @zhin.js/adapter@1.2.2
|
|
10
|
+
- @zhin.js/core@1.5.16
|
|
11
|
+
- zhin.js@7.0.1
|
|
12
|
+
- @zhin.js/agent@1.2.1
|
|
13
|
+
|
|
3
14
|
## 9.0.0
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { Endpoint } from 'zhin.js/adapter';
|
|
2
1
|
/**
|
|
3
2
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
4
3
|
*/
|
|
5
|
-
import { type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
|
|
4
|
+
import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
|
|
6
5
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
7
6
|
import type { CapabilityId } from 'zhin.js';
|
|
8
7
|
import { type QqInboundMessage, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig } from './protocol.js';
|
package/lib/endpoint.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { Endpoint } from 'zhin.js/adapter';
|
|
2
1
|
/**
|
|
3
2
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
4
3
|
*/
|
|
5
|
-
import { createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
4
|
+
import { Endpoint, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
6
5
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
7
6
|
import { formatOutbound } from './outbound.js';
|
|
8
7
|
import { formatInboundContent, parseCompoundMessageId, qqInboundConversation, qqOutboundKind, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
|
|
@@ -15,12 +14,14 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
15
14
|
#createBot;
|
|
16
15
|
#bot = null;
|
|
17
16
|
#open = false;
|
|
18
|
-
#
|
|
17
|
+
#lifecycle;
|
|
18
|
+
#cleanup;
|
|
19
19
|
management = createQqEndpointManagement(() => this.#requireBot());
|
|
20
20
|
control = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
21
21
|
constructor(options) {
|
|
22
22
|
super();
|
|
23
23
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
24
|
+
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
24
25
|
this.#options = options;
|
|
25
26
|
this.#createBot = options.createBot ?? defaultCreateBot;
|
|
26
27
|
}
|
|
@@ -32,21 +33,51 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
32
33
|
return this.#options.config.id;
|
|
33
34
|
}
|
|
34
35
|
async start() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
await this.#lifecycle.start(async (handle) => {
|
|
37
|
+
const bot = this.#createBot(this.#options.config);
|
|
38
|
+
this.#bot = bot;
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
let stopping;
|
|
41
|
+
const stopBot = async () => {
|
|
42
|
+
try {
|
|
43
|
+
bot.removeAllListeners();
|
|
44
|
+
}
|
|
45
|
+
catch { /* continue SDK cleanup */ }
|
|
46
|
+
try {
|
|
47
|
+
await bot.stop();
|
|
48
|
+
}
|
|
49
|
+
catch { /* best-effort SDK cleanup */ }
|
|
50
|
+
};
|
|
51
|
+
const cleanup = () => {
|
|
52
|
+
cancelled = true;
|
|
53
|
+
if (this.#bot === bot)
|
|
54
|
+
this.#bot = null;
|
|
55
|
+
return stopping ??= stopBot();
|
|
56
|
+
};
|
|
57
|
+
this.#cleanup = cleanup;
|
|
58
|
+
handle.onForceClose(() => { void cleanup(); });
|
|
59
|
+
try {
|
|
60
|
+
this.#bindBot(bot);
|
|
61
|
+
await bot.start();
|
|
62
|
+
if (cancelled) {
|
|
63
|
+
// SDK startup may create resources after stop; close this exact old client again.
|
|
64
|
+
await stopping;
|
|
65
|
+
await stopBot();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.#logger.info(`connected (websocket) | appid: ${this.#options.config.appid}`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const wasCancelled = cancelled;
|
|
72
|
+
await cleanup();
|
|
73
|
+
if (wasCancelled) {
|
|
74
|
+
await stopBot();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
78
|
+
throw new Error(`QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`, { cause: error });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
50
81
|
}
|
|
51
82
|
open() {
|
|
52
83
|
this.#open = true;
|
|
@@ -56,17 +87,10 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
56
87
|
}
|
|
57
88
|
async stop() {
|
|
58
89
|
this.#open = false;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
/* ignore */
|
|
66
|
-
}
|
|
67
|
-
this.#bot = null;
|
|
68
|
-
}
|
|
69
|
-
this.#started = false;
|
|
90
|
+
const cleanup = this.#cleanup;
|
|
91
|
+
this.#cleanup = undefined;
|
|
92
|
+
await this.#lifecycle.stop();
|
|
93
|
+
await cleanup?.();
|
|
70
94
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
71
95
|
}
|
|
72
96
|
async send({ conversation, payload }) {
|
|
@@ -158,12 +182,16 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
158
182
|
}
|
|
159
183
|
#bindBot(bot) {
|
|
160
184
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
185
|
+
if (this.#bot !== bot)
|
|
186
|
+
return;
|
|
161
187
|
this.#emitPlatformEvent('message', raw);
|
|
162
188
|
const msg = normalizeQqMessage(raw);
|
|
163
189
|
if (msg)
|
|
164
190
|
this.admit(msg);
|
|
165
191
|
});
|
|
166
192
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
193
|
+
if (this.#bot !== bot)
|
|
194
|
+
return;
|
|
167
195
|
this.#emitPlatformEvent(eventName, raw);
|
|
168
196
|
receiveQqSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
169
197
|
});
|
|
@@ -189,14 +217,15 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
189
217
|
#options;
|
|
190
218
|
#createBot;
|
|
191
219
|
#bot = null;
|
|
192
|
-
#routeReleases = [];
|
|
193
220
|
#open = false;
|
|
194
|
-
#
|
|
221
|
+
#lifecycle;
|
|
222
|
+
#cleanup;
|
|
195
223
|
management = createQqEndpointManagement(() => this.#requireBot());
|
|
196
224
|
control = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
197
225
|
constructor(options) {
|
|
198
226
|
super();
|
|
199
227
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
228
|
+
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
200
229
|
this.#options = options;
|
|
201
230
|
this.#createBot = options.createBot ?? defaultCreateHttpBot;
|
|
202
231
|
}
|
|
@@ -208,21 +237,57 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
208
237
|
return this.#options.config.id;
|
|
209
238
|
}
|
|
210
239
|
async start() {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
240
|
+
await this.#lifecycle.start(async (handle) => {
|
|
241
|
+
const bot = this.#createBot(this.#options.config);
|
|
242
|
+
this.#bot = bot;
|
|
243
|
+
let cancelled = false;
|
|
244
|
+
let stopping;
|
|
245
|
+
const routes = [];
|
|
246
|
+
const stopBot = async () => {
|
|
247
|
+
try {
|
|
248
|
+
bot.removeAllListeners();
|
|
249
|
+
}
|
|
250
|
+
catch { /* continue SDK cleanup */ }
|
|
251
|
+
try {
|
|
252
|
+
await bot.stop();
|
|
253
|
+
}
|
|
254
|
+
catch { /* best-effort SDK cleanup */ }
|
|
255
|
+
};
|
|
256
|
+
const cleanup = () => {
|
|
257
|
+
cancelled = true;
|
|
258
|
+
if (this.#bot === bot)
|
|
259
|
+
this.#bot = null;
|
|
260
|
+
for (const release of routes.splice(0))
|
|
261
|
+
release();
|
|
262
|
+
return stopping ??= stopBot();
|
|
263
|
+
};
|
|
264
|
+
this.#cleanup = cleanup;
|
|
265
|
+
handle.onForceClose(() => { void cleanup(); });
|
|
266
|
+
try {
|
|
267
|
+
this.#bindBot(bot);
|
|
268
|
+
routes.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
269
|
+
config: this.#options.config,
|
|
270
|
+
getBot: () => cancelled ? null : bot,
|
|
271
|
+
}));
|
|
272
|
+
await bot.start();
|
|
273
|
+
if (cancelled) {
|
|
274
|
+
// SDK startup may create resources after stop; close this exact old client again.
|
|
275
|
+
await stopping;
|
|
276
|
+
await stopBot();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const wasCancelled = cancelled;
|
|
283
|
+
await cleanup();
|
|
284
|
+
if (wasCancelled) {
|
|
285
|
+
await stopBot();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
226
291
|
}
|
|
227
292
|
open() {
|
|
228
293
|
this.#open = true;
|
|
@@ -232,19 +297,10 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
232
297
|
}
|
|
233
298
|
async stop() {
|
|
234
299
|
this.#open = false;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
this.#bot.removeAllListeners();
|
|
240
|
-
await this.#bot.stop();
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
/* ignore */
|
|
244
|
-
}
|
|
245
|
-
this.#bot = null;
|
|
246
|
-
}
|
|
247
|
-
this.#started = false;
|
|
300
|
+
const cleanup = this.#cleanup;
|
|
301
|
+
this.#cleanup = undefined;
|
|
302
|
+
await this.#lifecycle.stop();
|
|
303
|
+
await cleanup?.();
|
|
248
304
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
249
305
|
}
|
|
250
306
|
async send({ conversation, payload }) {
|
|
@@ -333,20 +389,18 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
333
389
|
}));
|
|
334
390
|
});
|
|
335
391
|
}
|
|
336
|
-
#setupRoutes() {
|
|
337
|
-
this.#routeReleases.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
338
|
-
config: this.#options.config,
|
|
339
|
-
getBot: () => this.#bot,
|
|
340
|
-
}));
|
|
341
|
-
}
|
|
342
392
|
#bindBot(bot) {
|
|
343
393
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
394
|
+
if (this.#bot !== bot)
|
|
395
|
+
return;
|
|
344
396
|
this.#emitPlatformEvent('message', raw);
|
|
345
397
|
const msg = normalizeQqMessage(raw);
|
|
346
398
|
if (msg)
|
|
347
399
|
this.admit(msg);
|
|
348
400
|
});
|
|
349
401
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
402
|
+
if (this.#bot !== bot)
|
|
403
|
+
return;
|
|
350
404
|
this.#emitPlatformEvent(eventName, raw);
|
|
351
405
|
receiveQqSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
352
406
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-qq",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.1",
|
|
4
4
|
"description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"qq-official-bot": "^1.2.3",
|
|
38
|
-
"@zhin.js/adapter": "1.2.
|
|
39
|
-
"@zhin.js/core": "1.5.
|
|
38
|
+
"@zhin.js/adapter": "1.2.2",
|
|
39
|
+
"@zhin.js/core": "1.5.16",
|
|
40
40
|
"@zhin.js/feature-kit": "1.0.13",
|
|
41
41
|
"@zhin.js/host-http": "1.0.17",
|
|
42
42
|
"@zhin.js/im-contract": "1.0.4",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"zod": "^4.0.0",
|
|
48
|
-
"@zhin.js/adapter": "1.2.
|
|
49
|
-
"@zhin.js/agent": "1.2.
|
|
48
|
+
"@zhin.js/adapter": "1.2.2",
|
|
49
|
+
"@zhin.js/agent": "1.2.1",
|
|
50
50
|
"@zhin.js/command": "1.0.16",
|
|
51
|
-
"@zhin.js/core": "1.5.
|
|
51
|
+
"@zhin.js/core": "1.5.16",
|
|
52
52
|
"@zhin.js/middleware": "1.0.13",
|
|
53
|
-
"zhin.js": "7.0.
|
|
53
|
+
"zhin.js": "7.0.1"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@zhin.js/agent": {
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"typescript": "^6.0.3",
|
|
75
75
|
"vitest": "^4.1.10",
|
|
76
76
|
"zod": "^4.4.3",
|
|
77
|
-
"@zhin.js/agent": "1.2.
|
|
77
|
+
"@zhin.js/agent": "1.2.1",
|
|
78
78
|
"@zhin.js/host-http": "1.0.17",
|
|
79
|
-
"zhin.js": "7.0.
|
|
79
|
+
"zhin.js": "7.0.1"
|
|
80
80
|
},
|
|
81
81
|
"files": [
|
|
82
82
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Endpoint } from 'zhin.js/adapter';
|
|
2
1
|
/**
|
|
3
2
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
4
3
|
*/
|
|
5
4
|
import {
|
|
5
|
+
Endpoint, createEndpointLifecycle, type EndpointLifecycle,
|
|
6
6
|
createRecallEndpointControl,
|
|
7
7
|
type EndpointControl,
|
|
8
8
|
type EndpointChannel,
|
|
@@ -59,13 +59,15 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
59
59
|
readonly #createBot: CreateQqBot;
|
|
60
60
|
#bot: QqBotTransport | null = null;
|
|
61
61
|
#open = false;
|
|
62
|
-
#
|
|
62
|
+
readonly #lifecycle: EndpointLifecycle;
|
|
63
|
+
#cleanup?: () => Promise<void>;
|
|
63
64
|
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot());
|
|
64
65
|
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
65
66
|
|
|
66
67
|
constructor(options: QqEndpointOptions) {
|
|
67
68
|
super();
|
|
68
69
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
70
|
+
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
69
71
|
this.#options = options;
|
|
70
72
|
this.#createBot = options.createBot ?? defaultCreateBot;
|
|
71
73
|
}
|
|
@@ -80,24 +82,46 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
async start(): Promise<void> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
await this.#lifecycle.start(async (handle) => {
|
|
86
|
+
const bot = this.#createBot(this.#options.config);
|
|
87
|
+
this.#bot = bot;
|
|
88
|
+
let cancelled = false;
|
|
89
|
+
let stopping: Promise<void> | undefined;
|
|
90
|
+
const stopBot = async () => {
|
|
91
|
+
try { bot.removeAllListeners(); } catch { /* continue SDK cleanup */ }
|
|
92
|
+
try { await bot.stop(); } catch { /* best-effort SDK cleanup */ }
|
|
93
|
+
};
|
|
94
|
+
const cleanup = () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
if (this.#bot === bot) this.#bot = null;
|
|
97
|
+
return stopping ??= stopBot();
|
|
98
|
+
};
|
|
99
|
+
this.#cleanup = cleanup;
|
|
100
|
+
handle.onForceClose(() => { void cleanup(); });
|
|
101
|
+
try {
|
|
102
|
+
this.#bindBot(bot);
|
|
103
|
+
await bot.start();
|
|
104
|
+
if (cancelled) {
|
|
105
|
+
// SDK startup may create resources after stop; close this exact old client again.
|
|
106
|
+
await stopping;
|
|
107
|
+
await stopBot();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.#logger.info(`connected (websocket) | appid: ${this.#options.config.appid}`);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const wasCancelled = cancelled;
|
|
113
|
+
await cleanup();
|
|
114
|
+
if (wasCancelled) {
|
|
115
|
+
await stopBot();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
119
|
+
throw new Error(
|
|
120
|
+
`QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`,
|
|
121
|
+
{ cause: error },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
open(): void {
|
|
@@ -110,16 +134,10 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
110
134
|
|
|
111
135
|
async stop(): Promise<void> {
|
|
112
136
|
this.#open = false;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} catch {
|
|
118
|
-
/* ignore */
|
|
119
|
-
}
|
|
120
|
-
this.#bot = null;
|
|
121
|
-
}
|
|
122
|
-
this.#started = false;
|
|
137
|
+
const cleanup = this.#cleanup;
|
|
138
|
+
this.#cleanup = undefined;
|
|
139
|
+
await this.#lifecycle.stop();
|
|
140
|
+
await cleanup?.();
|
|
123
141
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
124
142
|
}
|
|
125
143
|
|
|
@@ -217,11 +235,13 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
217
235
|
|
|
218
236
|
#bindBot(bot: QqBotTransport): void {
|
|
219
237
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
238
|
+
if (this.#bot !== bot) return;
|
|
220
239
|
this.#emitPlatformEvent('message', raw);
|
|
221
240
|
const msg = normalizeQqMessage(raw);
|
|
222
241
|
if (msg) this.admit(msg);
|
|
223
242
|
});
|
|
224
243
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
244
|
+
if (this.#bot !== bot) return;
|
|
225
245
|
this.#emitPlatformEvent(eventName, raw);
|
|
226
246
|
receiveQqSideEvent(
|
|
227
247
|
(name, payload) => this.emit(name, payload),
|
|
@@ -264,15 +284,16 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
264
284
|
readonly #options: QqHttpEndpointOptions;
|
|
265
285
|
readonly #createBot: CreateQqHttpBot;
|
|
266
286
|
#bot: QqHttpBotTransport | null = null;
|
|
267
|
-
#routeReleases: ReturnType<typeof registerQqWebhookRoutes> = [];
|
|
268
287
|
#open = false;
|
|
269
|
-
#
|
|
288
|
+
readonly #lifecycle: EndpointLifecycle;
|
|
289
|
+
#cleanup?: () => Promise<void>;
|
|
270
290
|
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot());
|
|
271
291
|
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
272
292
|
|
|
273
293
|
constructor(options: QqHttpEndpointOptions) {
|
|
274
294
|
super();
|
|
275
295
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
296
|
+
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
276
297
|
this.#options = options;
|
|
277
298
|
this.#createBot = options.createBot ?? defaultCreateHttpBot;
|
|
278
299
|
}
|
|
@@ -287,21 +308,48 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
287
308
|
}
|
|
288
309
|
|
|
289
310
|
async start(): Promise<void> {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
await this.#lifecycle.start(async (handle) => {
|
|
312
|
+
const bot = this.#createBot(this.#options.config);
|
|
313
|
+
this.#bot = bot;
|
|
314
|
+
let cancelled = false;
|
|
315
|
+
let stopping: Promise<void> | undefined;
|
|
316
|
+
const routes: Array<() => void> = [];
|
|
317
|
+
const stopBot = async () => {
|
|
318
|
+
try { bot.removeAllListeners(); } catch { /* continue SDK cleanup */ }
|
|
319
|
+
try { await bot.stop(); } catch { /* best-effort SDK cleanup */ }
|
|
320
|
+
};
|
|
321
|
+
const cleanup = () => {
|
|
322
|
+
cancelled = true;
|
|
323
|
+
if (this.#bot === bot) this.#bot = null;
|
|
324
|
+
for (const release of routes.splice(0)) release();
|
|
325
|
+
return stopping ??= stopBot();
|
|
326
|
+
};
|
|
327
|
+
this.#cleanup = cleanup;
|
|
328
|
+
handle.onForceClose(() => { void cleanup(); });
|
|
329
|
+
try {
|
|
330
|
+
this.#bindBot(bot);
|
|
331
|
+
routes.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
332
|
+
config: this.#options.config,
|
|
333
|
+
getBot: () => cancelled ? null : bot,
|
|
334
|
+
}));
|
|
335
|
+
await bot.start();
|
|
336
|
+
if (cancelled) {
|
|
337
|
+
// SDK startup may create resources after stop; close this exact old client again.
|
|
338
|
+
await stopping;
|
|
339
|
+
await stopBot();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const wasCancelled = cancelled;
|
|
345
|
+
await cleanup();
|
|
346
|
+
if (wasCancelled) {
|
|
347
|
+
await stopBot();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
305
353
|
}
|
|
306
354
|
|
|
307
355
|
open(): void {
|
|
@@ -314,17 +362,10 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
314
362
|
|
|
315
363
|
async stop(): Promise<void> {
|
|
316
364
|
this.#open = false;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await this.#bot.stop();
|
|
322
|
-
} catch {
|
|
323
|
-
/* ignore */
|
|
324
|
-
}
|
|
325
|
-
this.#bot = null;
|
|
326
|
-
}
|
|
327
|
-
this.#started = false;
|
|
365
|
+
const cleanup = this.#cleanup;
|
|
366
|
+
this.#cleanup = undefined;
|
|
367
|
+
await this.#lifecycle.stop();
|
|
368
|
+
await cleanup?.();
|
|
328
369
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
329
370
|
}
|
|
330
371
|
|
|
@@ -419,20 +460,15 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
419
460
|
});
|
|
420
461
|
}
|
|
421
462
|
|
|
422
|
-
#setupRoutes(): void {
|
|
423
|
-
this.#routeReleases.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
424
|
-
config: this.#options.config,
|
|
425
|
-
getBot: () => this.#bot,
|
|
426
|
-
}));
|
|
427
|
-
}
|
|
428
|
-
|
|
429
463
|
#bindBot(bot: QqBotTransport): void {
|
|
430
464
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
465
|
+
if (this.#bot !== bot) return;
|
|
431
466
|
this.#emitPlatformEvent('message', raw);
|
|
432
467
|
const msg = normalizeQqMessage(raw);
|
|
433
468
|
if (msg) this.admit(msg);
|
|
434
469
|
});
|
|
435
470
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
471
|
+
if (this.#bot !== bot) return;
|
|
436
472
|
this.#emitPlatformEvent(eventName, raw);
|
|
437
473
|
receiveQqSideEvent(
|
|
438
474
|
(name, payload) => this.emit(name, payload),
|