@zhin.js/adapter-qq 1.1.18 → 1.1.19
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 +39 -0
- package/README.md +21 -18
- package/adapters/qq/index.js +4 -38
- package/adapters/qq/index.ts +3 -46
- package/agents/qq/conversation.js +1 -1
- package/agents/qq/conversation.ts +1 -1
- package/agents/qq/skills/qq/tools/add_role/index.js +2 -1
- package/agents/qq/skills/qq/tools/add_role/index.ts +2 -1
- package/agents/qq/skills/qq/tools/channel_info/index.js +1 -1
- package/agents/qq/skills/qq/tools/channel_info/index.ts +1 -1
- package/agents/qq/skills/qq/tools/create_role/index.js +1 -1
- package/agents/qq/skills/qq/tools/create_role/index.ts +1 -1
- package/agents/qq/skills/qq/tools/guild_kick_member/index.js +1 -1
- package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +1 -1
- package/agents/qq/skills/qq/tools/guild_mute_member/index.js +1 -1
- package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +1 -1
- package/agents/qq/skills/qq/tools/list_channels/index.js +1 -1
- package/agents/qq/skills/qq/tools/list_channels/index.ts +1 -1
- package/agents/qq/skills/qq/tools/list_roles/index.js +1 -1
- package/agents/qq/skills/qq/tools/list_roles/index.ts +1 -1
- package/agents/qq/skills/qq/tools/member_detail/index.js +1 -1
- package/agents/qq/skills/qq/tools/member_detail/index.ts +1 -1
- package/agents/qq/skills/qq/tools/remove_role/index.js +2 -1
- package/agents/qq/skills/qq/tools/remove_role/index.ts +2 -1
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +1 -1
- package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +1 -1
- package/agents/qq/skills/qq-directory/tools/list_guilds/index.js +1 -1
- package/agents/qq/skills/qq-directory/tools/list_guilds/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +2 -2
- package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +2 -2
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +1 -1
- package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +1 -1
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +2 -2
- package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +2 -2
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +1 -1
- package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +1 -1
- package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +1 -1
- package/commands/qq/endpoint/definition.js +41 -36
- package/commands/qq/endpoint/definition.ts +41 -67
- package/lib/adapter.d.ts +13 -0
- package/lib/adapter.js +27 -0
- package/lib/client.d.ts +7 -9
- package/lib/client.js +1 -2
- package/lib/console.d.ts +1 -6
- package/lib/console.js +29 -20
- package/lib/endpoint.d.ts +8 -27
- package/lib/endpoint.js +218 -165
- package/lib/group-admin.d.ts +14 -9
- package/lib/group-admin.js +21 -20
- package/lib/i18n.js +4 -4
- package/lib/image-size.d.ts +6 -1
- package/lib/image-size.js +2 -3
- package/lib/index.d.ts +5 -4
- package/lib/index.js +3 -3
- package/lib/interaction.js +1 -1
- package/lib/outbound.d.ts +4 -1
- package/lib/outbound.js +3 -2
- package/lib/protocol.d.ts +2 -0
- package/lib/qq-console-spec.js +12 -1
- package/lib/qq-console.d.ts +3 -4
- package/lib/qq-console.js +16 -6
- package/lib/qq-endpoint-write.d.ts +6 -10
- package/lib/qq-endpoint-write.js +5 -32
- package/lib/qq-runtime-state.d.ts +11 -10
- package/lib/qq-runtime-state.js +4 -5
- package/lib/qq-send.d.ts +7 -14
- package/lib/qq-send.js +20 -6
- package/lib/webhook.d.ts +3 -6
- package/lib/webhook.js +6 -49
- package/lib/ws.d.ts +10 -33
- package/lib/ws.js +21 -70
- package/middlewares/bot-kind-reply/index.js +1 -2
- package/middlewares/bot-kind-reply/index.ts +0 -2
- package/package.json +19 -19
- package/plugin.js +5 -5
- package/src/adapter.ts +23 -0
- package/src/client.ts +4 -12
- package/src/console.ts +27 -20
- package/src/endpoint.ts +218 -160
- package/src/group-admin.ts +28 -26
- package/src/i18n.ts +4 -4
- package/src/image-size.ts +7 -3
- package/src/index.ts +5 -5
- package/src/interaction.ts +1 -1
- package/src/outbound.ts +3 -2
- package/src/protocol.ts +2 -0
- package/src/qq-console-spec.ts +12 -1
- package/src/qq-console.ts +14 -8
- package/src/qq-endpoint-write.ts +10 -38
- package/src/qq-runtime-state.ts +11 -16
- package/src/qq-send.ts +28 -12
- package/src/webhook.ts +7 -57
- package/src/ws.ts +27 -105
package/lib/endpoint.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Bot } from 'qq-official-bot';
|
|
1
2
|
import { installQqUploadDiagnostics } from './upload-diagnostics.js';
|
|
2
3
|
import { projectQqGuildSenderRoles } from './platform-permit.js';
|
|
3
4
|
import { qqDeliveryFailure } from './delivery-error.js';
|
|
@@ -8,21 +9,24 @@ import { inspectQqGroupMember, inspectQqGroupMemberProfile, inspectQqGroupScene,
|
|
|
8
9
|
/**
|
|
9
10
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
10
11
|
*/
|
|
11
|
-
import {
|
|
12
|
+
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createEndpointOperation, } from 'zhin.js/adapter';
|
|
12
13
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
13
14
|
import { collectOutboundImageSizes, formatOutboundMessages } from './outbound.js';
|
|
14
15
|
import { QqActiveMessageGate, QqPassiveReplyLedger } from './passive-reply.js';
|
|
15
16
|
import { sendQqMessages } from './qq-send.js';
|
|
16
17
|
import { formatInboundContent, parseCompoundMessageId, qqInboundConversation, qqOutboundKind, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
|
|
17
|
-
import {
|
|
18
|
-
import { bindQqBotInboundEvents,
|
|
18
|
+
import { qqHttpBotOptions, registerQqWebhookRoutes, } from './webhook.js';
|
|
19
|
+
import { bindQqBotInboundEvents, qqWebsocketBotOptions, configureQqWebsocketReceiver, disposeQqAuthorization, normalizeQqMessage, } from './ws.js';
|
|
19
20
|
import { bindQqBotSideEvents, receiveQqSideEvent, } from './side-event-dispatch.js';
|
|
20
|
-
export class QqWebsocketEndpoint extends
|
|
21
|
+
export class QqWebsocketEndpoint extends Bot {
|
|
22
|
+
[endpointSpec];
|
|
23
|
+
#receiverResources;
|
|
21
24
|
#logger;
|
|
22
25
|
#options;
|
|
23
|
-
#
|
|
24
|
-
#
|
|
26
|
+
#connection;
|
|
27
|
+
#stopping;
|
|
25
28
|
#open = false;
|
|
29
|
+
#executionSignal;
|
|
26
30
|
#offlineNoticeAt = 0;
|
|
27
31
|
#seenInteractions = new Map();
|
|
28
32
|
/** Recent msg_id / event_id per conversation, so replies go out as passive messages. */
|
|
@@ -30,72 +34,71 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
30
34
|
#activeGate = new QqActiveMessageGate();
|
|
31
35
|
#lifecycle;
|
|
32
36
|
#cleanup;
|
|
33
|
-
management = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#
|
|
34
|
-
control = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
37
|
+
#management = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#connection !== undefined);
|
|
38
|
+
#control = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
35
39
|
constructor(options) {
|
|
36
|
-
super();
|
|
40
|
+
super(qqWebsocketBotOptions(options.config));
|
|
37
41
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
38
42
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id });
|
|
39
43
|
this.#options = options;
|
|
40
|
-
this.#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
this.#receiverResources = configureQqWebsocketReceiver(this, options.config);
|
|
45
|
+
const endpoint = this;
|
|
46
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
47
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
48
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
49
|
+
get transportState() { return endpoint.#transportState; }, management: this.#management, control: this.#control,
|
|
50
|
+
});
|
|
51
|
+
this.#bindBot(this);
|
|
44
52
|
}
|
|
45
53
|
/** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
|
|
46
54
|
get name() {
|
|
47
55
|
return this.#options.config.id;
|
|
48
56
|
}
|
|
49
|
-
get transportState() {
|
|
57
|
+
get #transportState() {
|
|
50
58
|
if (this.#lifecycle.state !== 'open')
|
|
51
59
|
return this.#lifecycle.state;
|
|
52
|
-
return this.#
|
|
60
|
+
return this.#receiverResources.state();
|
|
53
61
|
}
|
|
54
|
-
async
|
|
62
|
+
async #startFramework(executionSignal) {
|
|
63
|
+
this.#executionSignal = executionSignal;
|
|
55
64
|
await this.#lifecycle.start(async (handle) => {
|
|
56
|
-
const bot = this
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
const bot = this;
|
|
66
|
+
const previousCleanup = this.#cleanup;
|
|
67
|
+
await previousCleanup?.();
|
|
68
|
+
const releaseUploadDiagnostics = installQqUploadDiagnostics(bot, metadata => this.#logger.debug(formatCompact(metadata)));
|
|
59
69
|
let cancelled = false;
|
|
60
70
|
const startupAbort = new AbortController();
|
|
71
|
+
this.#connection = startupAbort;
|
|
72
|
+
let nativeStart;
|
|
61
73
|
let stopping;
|
|
74
|
+
let transportClose;
|
|
62
75
|
const stopBot = async () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
catch { /* continue SDK cleanup */ }
|
|
67
|
-
try {
|
|
68
|
-
await bot.stop();
|
|
69
|
-
}
|
|
70
|
-
catch { /* best-effort SDK cleanup */ }
|
|
76
|
+
await super.stop();
|
|
77
|
+
await nativeStart?.catch(() => undefined);
|
|
71
78
|
};
|
|
72
79
|
const cleanup = () => {
|
|
73
80
|
cancelled = true;
|
|
74
81
|
releaseUploadDiagnostics();
|
|
75
82
|
startupAbort.abort();
|
|
76
|
-
if (
|
|
77
|
-
this
|
|
83
|
+
if (transportClose)
|
|
84
|
+
this.receiver.off('close', transportClose);
|
|
85
|
+
if (this.#connection === startupAbort)
|
|
86
|
+
this.#connection = undefined;
|
|
78
87
|
return stopping ??= stopBot();
|
|
79
88
|
};
|
|
80
89
|
this.#cleanup = cleanup;
|
|
81
|
-
handle.onForceClose(() => { void cleanup(); });
|
|
90
|
+
handle.onForceClose(() => { void cleanup().catch(() => undefined); });
|
|
82
91
|
try {
|
|
83
|
-
|
|
84
|
-
if (cancelled || this.#
|
|
92
|
+
transportClose = () => {
|
|
93
|
+
if (cancelled || this.#connection === undefined)
|
|
85
94
|
return;
|
|
86
95
|
this.#notifyOffline('transport_close');
|
|
87
|
-
void cleanup();
|
|
96
|
+
void cleanup().catch(() => undefined);
|
|
88
97
|
handle.notifyClosed();
|
|
89
|
-
}
|
|
90
|
-
this
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
await stopBot();
|
|
94
|
-
}, async (error) => {
|
|
95
|
-
if (cancelled)
|
|
96
|
-
await stopBot();
|
|
97
|
-
throw error;
|
|
98
|
-
}), startupAbort.signal);
|
|
98
|
+
};
|
|
99
|
+
this.receiver.on('close', transportClose);
|
|
100
|
+
nativeStart = super.start();
|
|
101
|
+
await awaitQqStartup(nativeStart.then(() => undefined), startupAbort.signal);
|
|
99
102
|
if (cancelled) {
|
|
100
103
|
// SDK startup may create resources after stop; close this exact old client again.
|
|
101
104
|
await stopping;
|
|
@@ -115,24 +118,35 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
115
118
|
}
|
|
116
119
|
});
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
#openFramework() {
|
|
119
122
|
this.#open = true;
|
|
120
123
|
}
|
|
121
|
-
|
|
124
|
+
#closeFramework() {
|
|
122
125
|
this.#open = false;
|
|
123
126
|
}
|
|
124
|
-
async
|
|
125
|
-
this.#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
async #stopFramework() {
|
|
128
|
+
this.#stopping ??= Promise.resolve().then(async () => {
|
|
129
|
+
this.#open = false;
|
|
130
|
+
const cleanup = this.#cleanup;
|
|
131
|
+
this.#cleanup = undefined;
|
|
132
|
+
try {
|
|
133
|
+
await this.#lifecycle.stop();
|
|
134
|
+
await cleanup?.();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
this.removeAllListeners(undefined);
|
|
138
|
+
this.#receiverResources.dispose();
|
|
139
|
+
disposeQqAuthorization(this);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
await this.#stopping;
|
|
130
143
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
131
144
|
}
|
|
132
|
-
async
|
|
145
|
+
async #sendFramework({ conversation, payload, signal }) {
|
|
133
146
|
// 媒体与 markdown/按钮不能同条发送(msg_type 7 与 2 互斥):公网图片嵌入 markdown,
|
|
134
147
|
// 拿不到 URL/尺寸的媒体降级为独立消息先发
|
|
135
|
-
const
|
|
148
|
+
const operation = createEndpointOperation({ signal });
|
|
149
|
+
const messages = formatOutboundMessages(payload, { imageSizes: await operation.prepare(() => collectOutboundImageSizes(payload, { signal })) });
|
|
136
150
|
const body = messages[messages.length - 1];
|
|
137
151
|
const kind = qqOutboundKind(conversation);
|
|
138
152
|
const bot = this.#requireBot();
|
|
@@ -143,7 +157,7 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
143
157
|
ledger: this.#passiveReplies,
|
|
144
158
|
activeGate: this.#activeGate,
|
|
145
159
|
log: fields => this.#logger.warn(formatCompact(fields)),
|
|
146
|
-
}, conversation, messages);
|
|
160
|
+
}, { conversation, messages, signal });
|
|
147
161
|
}
|
|
148
162
|
catch (error) {
|
|
149
163
|
const { failure, diagnostic } = qqDeliveryFailure(error);
|
|
@@ -159,7 +173,7 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
159
173
|
async recallMessage(messageId) {
|
|
160
174
|
const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
|
|
161
175
|
const bot = this.#requireBot();
|
|
162
|
-
await recallQqMessage(bot, kind, channelId, qqMsgId);
|
|
176
|
+
await recallQqMessage(bot, { kind, channelId, messageId: qqMsgId });
|
|
163
177
|
this.#logger.debug(formatCompact({
|
|
164
178
|
op: 'qq_recall',
|
|
165
179
|
endpoint: this.#options.config.id,
|
|
@@ -180,8 +194,10 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
180
194
|
const roleProjection = projectQqGuildSenderRoles(msg.authorRoles);
|
|
181
195
|
if (msg.id)
|
|
182
196
|
this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { id: msg.id });
|
|
183
|
-
void this.emit('message.receive', {
|
|
197
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
184
198
|
conversation,
|
|
199
|
+
event: msg.event,
|
|
200
|
+
platformSentAt: msg.platformSentAt,
|
|
185
201
|
message: { conversation, id: msg.id },
|
|
186
202
|
content,
|
|
187
203
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
@@ -225,13 +241,16 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
225
241
|
if (this.#seenInteractions.size > 10_000)
|
|
226
242
|
this.#seenInteractions.delete(this.#seenInteractions.keys().next().value);
|
|
227
243
|
// ACK owns only platform loading state, never implies business acceptance.
|
|
228
|
-
void action.ack().then(ok => { if (ok === false)
|
|
229
|
-
this.#logger.warn('qq_interaction_ack_failed'); }, () => this.#
|
|
244
|
+
void this[endpointSpec].events.run(() => awaitOwnedAck(action.ack(), this.#executionSignal)).then(ok => { if (ok === false)
|
|
245
|
+
this.#logger.warn('qq_interaction_ack_failed'); }, () => { if (this.#executionSignal?.aborted)
|
|
246
|
+
this.#logger.debug('QQ ACK wait cancelled');
|
|
247
|
+
else
|
|
248
|
+
this.#logger.warn('qq_interaction_ack_failed'); });
|
|
230
249
|
const conversation = qqInboundConversation(String(this.#options.id), action.message);
|
|
231
250
|
// INTERACTION_CREATE's id is an event_id the reply may answer passively.
|
|
232
251
|
this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { event_id: action.message.id });
|
|
233
|
-
void this.emit('message.receive', {
|
|
234
|
-
conversation, message: { conversation, id: action.message.id }, content: '',
|
|
252
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
253
|
+
event: action.message.event, conversation, message: { conversation, id: action.message.id }, content: '',
|
|
235
254
|
segments: action.message.segments, sender: { id: action.message.authorId }, endpointId: this.#options.config.id,
|
|
236
255
|
metadata: Object.freeze({ eventType: 'INTERACTION_CREATE', sourceMessageIdAvailable: action.sourceMessageId !== undefined,
|
|
237
256
|
...(action.sourceMessageId ? { sourceMessageId: action.sourceMessageId } : {}),
|
|
@@ -261,24 +280,28 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
261
280
|
}
|
|
262
281
|
#bindBot(bot) {
|
|
263
282
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
void this[endpointSpec].events.run(() => {
|
|
284
|
+
if (this.#connection === undefined)
|
|
285
|
+
return;
|
|
286
|
+
this.#emitPlatformEvent('message', raw);
|
|
287
|
+
const msg = normalizeQqMessage(raw);
|
|
288
|
+
if (msg)
|
|
289
|
+
this.admit(msg);
|
|
290
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
270
291
|
});
|
|
271
292
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
293
|
+
void this[endpointSpec].events.run(() => {
|
|
294
|
+
if (this.#connection === undefined)
|
|
295
|
+
return;
|
|
296
|
+
this.#emitPlatformEvent(eventName, raw);
|
|
297
|
+
if (this.admitInteraction(raw))
|
|
298
|
+
return;
|
|
299
|
+
receiveQqSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
300
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
278
301
|
});
|
|
279
302
|
}
|
|
280
303
|
#emitPlatformEvent(name, event) {
|
|
281
|
-
void this.
|
|
304
|
+
void this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
282
305
|
this.#logger.warn(formatCompact({
|
|
283
306
|
op: 'qq_platform_event_failed',
|
|
284
307
|
event: name,
|
|
@@ -287,18 +310,20 @@ export class QqWebsocketEndpoint extends Endpoint {
|
|
|
287
310
|
});
|
|
288
311
|
}
|
|
289
312
|
#requireBot() {
|
|
290
|
-
if (!this.#
|
|
313
|
+
if (!this.#connection)
|
|
291
314
|
throw new EndpointDeliveryError('endpoint_disconnected', 'QQ bot not connected', 'not_sent');
|
|
292
|
-
return this
|
|
315
|
+
return this;
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
296
|
-
export class QqHttpEndpoint extends
|
|
319
|
+
export class QqHttpEndpoint extends Bot {
|
|
320
|
+
[endpointSpec];
|
|
297
321
|
#logger;
|
|
298
322
|
#options;
|
|
299
|
-
#
|
|
300
|
-
#
|
|
323
|
+
#connection;
|
|
324
|
+
#stopping;
|
|
301
325
|
#open = false;
|
|
326
|
+
#executionSignal;
|
|
302
327
|
#offlineNoticeAt = 0;
|
|
303
328
|
#seenInteractions = new Map();
|
|
304
329
|
/** Recent msg_id / event_id per conversation, so replies go out as passive messages. */
|
|
@@ -306,70 +331,65 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
306
331
|
#activeGate = new QqActiveMessageGate();
|
|
307
332
|
#lifecycle;
|
|
308
333
|
#cleanup;
|
|
309
|
-
management = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#
|
|
310
|
-
control = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
334
|
+
#management = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#connection !== undefined);
|
|
335
|
+
#control = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
311
336
|
constructor(options) {
|
|
312
|
-
super();
|
|
337
|
+
super(qqHttpBotOptions(options.config));
|
|
313
338
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
314
339
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
315
340
|
this.#options = options;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
341
|
+
const endpoint = this;
|
|
342
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
343
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
344
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
345
|
+
get transportState() { return endpoint.#transportState; },
|
|
346
|
+
management: this.#management, control: this.#control,
|
|
347
|
+
});
|
|
348
|
+
this.#bindBot(this);
|
|
320
349
|
}
|
|
321
350
|
/** Live endpoint 名(Console/AdapterIndex 展示用)。 */
|
|
322
351
|
get name() {
|
|
323
352
|
return this.#options.config.id;
|
|
324
353
|
}
|
|
325
|
-
get transportState() {
|
|
354
|
+
get #transportState() {
|
|
326
355
|
return this.#lifecycle.state;
|
|
327
356
|
}
|
|
328
|
-
async
|
|
357
|
+
async #startFramework(executionSignal) {
|
|
358
|
+
this.#executionSignal = executionSignal;
|
|
329
359
|
await this.#lifecycle.start(async (handle) => {
|
|
330
|
-
const bot = this
|
|
331
|
-
|
|
332
|
-
|
|
360
|
+
const bot = this;
|
|
361
|
+
const previousCleanup = this.#cleanup;
|
|
362
|
+
await previousCleanup?.();
|
|
363
|
+
const releaseUploadDiagnostics = installQqUploadDiagnostics(bot, metadata => this.#logger.debug(formatCompact(metadata)));
|
|
333
364
|
let cancelled = false;
|
|
334
365
|
const startupAbort = new AbortController();
|
|
366
|
+
this.#connection = startupAbort;
|
|
367
|
+
let nativeStart;
|
|
335
368
|
let stopping;
|
|
336
369
|
const routes = [];
|
|
337
370
|
const stopBot = async () => {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
catch { /* continue SDK cleanup */ }
|
|
342
|
-
try {
|
|
343
|
-
await bot.stop();
|
|
344
|
-
}
|
|
345
|
-
catch { /* best-effort SDK cleanup */ }
|
|
371
|
+
await super.stop();
|
|
372
|
+
await nativeStart?.catch(() => undefined);
|
|
346
373
|
};
|
|
347
374
|
const cleanup = () => {
|
|
348
375
|
cancelled = true;
|
|
349
376
|
releaseUploadDiagnostics();
|
|
350
377
|
startupAbort.abort();
|
|
351
|
-
if (this.#
|
|
352
|
-
this.#
|
|
378
|
+
if (this.#connection === startupAbort)
|
|
379
|
+
this.#connection = undefined;
|
|
353
380
|
for (const release of routes.splice(0))
|
|
354
381
|
release();
|
|
355
382
|
return stopping ??= stopBot();
|
|
356
383
|
};
|
|
357
384
|
this.#cleanup = cleanup;
|
|
358
|
-
handle.onForceClose(() => { void cleanup(); });
|
|
385
|
+
handle.onForceClose(() => { void cleanup().catch(() => undefined); });
|
|
359
386
|
try {
|
|
360
|
-
this.#bindBot(bot);
|
|
361
387
|
routes.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
362
388
|
config: this.#options.config,
|
|
363
389
|
getBot: () => cancelled ? null : bot,
|
|
364
390
|
}));
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
await stopBot();
|
|
368
|
-
}, async (error) => {
|
|
369
|
-
if (cancelled)
|
|
370
|
-
await stopBot();
|
|
371
|
-
throw error;
|
|
372
|
-
}), startupAbort.signal);
|
|
391
|
+
nativeStart = super.start();
|
|
392
|
+
await awaitQqStartup(nativeStart.then(() => undefined), startupAbort.signal);
|
|
373
393
|
if (cancelled) {
|
|
374
394
|
// SDK startup may create resources after stop; close this exact old client again.
|
|
375
395
|
await stopping;
|
|
@@ -388,24 +408,34 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
388
408
|
}
|
|
389
409
|
});
|
|
390
410
|
}
|
|
391
|
-
|
|
411
|
+
#openFramework() {
|
|
392
412
|
this.#open = true;
|
|
393
413
|
}
|
|
394
|
-
|
|
414
|
+
#closeFramework() {
|
|
395
415
|
this.#open = false;
|
|
396
416
|
}
|
|
397
|
-
async
|
|
398
|
-
this.#
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
417
|
+
async #stopFramework() {
|
|
418
|
+
this.#stopping ??= Promise.resolve().then(async () => {
|
|
419
|
+
this.#open = false;
|
|
420
|
+
const cleanup = this.#cleanup;
|
|
421
|
+
this.#cleanup = undefined;
|
|
422
|
+
try {
|
|
423
|
+
await this.#lifecycle.stop();
|
|
424
|
+
await cleanup?.();
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
this.removeAllListeners(undefined);
|
|
428
|
+
disposeQqAuthorization(this);
|
|
429
|
+
}
|
|
430
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
431
|
+
});
|
|
432
|
+
await this.#stopping;
|
|
404
433
|
}
|
|
405
|
-
async
|
|
434
|
+
async #sendFramework({ conversation, payload, signal }) {
|
|
406
435
|
// 媒体与 markdown/按钮不能同条发送(msg_type 7 与 2 互斥):公网图片嵌入 markdown,
|
|
407
436
|
// 拿不到 URL/尺寸的媒体降级为独立消息先发
|
|
408
|
-
const
|
|
437
|
+
const operation = createEndpointOperation({ signal });
|
|
438
|
+
const messages = formatOutboundMessages(payload, { imageSizes: await operation.prepare(() => collectOutboundImageSizes(payload, { signal })) });
|
|
409
439
|
const body = messages[messages.length - 1];
|
|
410
440
|
const kind = qqOutboundKind(conversation);
|
|
411
441
|
const bot = this.#requireBot();
|
|
@@ -416,7 +446,7 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
416
446
|
ledger: this.#passiveReplies,
|
|
417
447
|
activeGate: this.#activeGate,
|
|
418
448
|
log: fields => this.#logger.warn(formatCompact(fields)),
|
|
419
|
-
}, conversation, messages);
|
|
449
|
+
}, { conversation, messages, signal });
|
|
420
450
|
}
|
|
421
451
|
catch (error) {
|
|
422
452
|
const { failure, diagnostic } = qqDeliveryFailure(error);
|
|
@@ -432,7 +462,7 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
432
462
|
async recallMessage(messageId) {
|
|
433
463
|
const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
|
|
434
464
|
const bot = this.#requireBot();
|
|
435
|
-
await recallQqMessage(bot, kind, channelId, qqMsgId);
|
|
465
|
+
await recallQqMessage(bot, { kind, channelId, messageId: qqMsgId });
|
|
436
466
|
this.#logger.debug(formatCompact({
|
|
437
467
|
op: 'qq_recall',
|
|
438
468
|
endpoint: this.#options.config.id,
|
|
@@ -452,8 +482,10 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
452
482
|
const roleProjection = projectQqGuildSenderRoles(msg.authorRoles);
|
|
453
483
|
if (msg.id)
|
|
454
484
|
this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { id: msg.id });
|
|
455
|
-
void this.emit('message.receive', {
|
|
485
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
456
486
|
conversation,
|
|
487
|
+
event: msg.event,
|
|
488
|
+
platformSentAt: msg.platformSentAt,
|
|
457
489
|
message: { conversation, id: msg.id },
|
|
458
490
|
content,
|
|
459
491
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
@@ -497,13 +529,16 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
497
529
|
if (this.#seenInteractions.size > 10_000)
|
|
498
530
|
this.#seenInteractions.delete(this.#seenInteractions.keys().next().value);
|
|
499
531
|
// ACK owns only platform loading state, never implies business acceptance.
|
|
500
|
-
void action.ack().then(ok => { if (ok === false)
|
|
501
|
-
this.#logger.warn('qq_interaction_ack_failed'); }, () => this.#
|
|
532
|
+
void this[endpointSpec].events.run(() => awaitOwnedAck(action.ack(), this.#executionSignal)).then(ok => { if (ok === false)
|
|
533
|
+
this.#logger.warn('qq_interaction_ack_failed'); }, () => { if (this.#executionSignal?.aborted)
|
|
534
|
+
this.#logger.debug('QQ ACK wait cancelled');
|
|
535
|
+
else
|
|
536
|
+
this.#logger.warn('qq_interaction_ack_failed'); });
|
|
502
537
|
const conversation = qqInboundConversation(String(this.#options.id), action.message);
|
|
503
538
|
// INTERACTION_CREATE's id is an event_id the reply may answer passively.
|
|
504
539
|
this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { event_id: action.message.id });
|
|
505
|
-
void this.emit('message.receive', {
|
|
506
|
-
conversation, message: { conversation, id: action.message.id }, content: '',
|
|
540
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
541
|
+
event: action.message.event, conversation, message: { conversation, id: action.message.id }, content: '',
|
|
507
542
|
segments: action.message.segments, sender: { id: action.message.authorId }, endpointId: this.#options.config.id,
|
|
508
543
|
metadata: Object.freeze({ eventType: 'INTERACTION_CREATE', sourceMessageIdAvailable: action.sourceMessageId !== undefined,
|
|
509
544
|
...(action.sourceMessageId ? { sourceMessageId: action.sourceMessageId } : {}),
|
|
@@ -533,24 +568,28 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
533
568
|
}
|
|
534
569
|
#bindBot(bot) {
|
|
535
570
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
571
|
+
void this[endpointSpec].events.run(() => {
|
|
572
|
+
if (this.#connection === undefined)
|
|
573
|
+
return;
|
|
574
|
+
this.#emitPlatformEvent('message', raw);
|
|
575
|
+
const msg = normalizeQqMessage(raw);
|
|
576
|
+
if (msg)
|
|
577
|
+
this.admit(msg);
|
|
578
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
542
579
|
});
|
|
543
580
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
581
|
+
void this[endpointSpec].events.run(() => {
|
|
582
|
+
if (this.#connection === undefined)
|
|
583
|
+
return;
|
|
584
|
+
this.#emitPlatformEvent(eventName, raw);
|
|
585
|
+
if (this.admitInteraction(raw))
|
|
586
|
+
return;
|
|
587
|
+
receiveQqSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, bot, eventName, raw, this.#logger);
|
|
588
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
550
589
|
});
|
|
551
590
|
}
|
|
552
591
|
#emitPlatformEvent(name, event) {
|
|
553
|
-
void this.
|
|
592
|
+
void this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
554
593
|
this.#logger.warn(formatCompact({
|
|
555
594
|
op: 'qq_platform_event_failed',
|
|
556
595
|
event: name,
|
|
@@ -559,39 +598,41 @@ export class QqHttpEndpoint extends Endpoint {
|
|
|
559
598
|
});
|
|
560
599
|
}
|
|
561
600
|
#requireBot() {
|
|
562
|
-
if (!this.#
|
|
601
|
+
if (!this.#connection)
|
|
563
602
|
throw new EndpointDeliveryError('endpoint_disconnected', 'QQ bot not connected', 'not_sent');
|
|
564
|
-
return this
|
|
603
|
+
return this;
|
|
565
604
|
}
|
|
566
605
|
}
|
|
567
606
|
function createQqEndpointManagement(requireClient, isActive) {
|
|
568
|
-
const api = () => requireClient()
|
|
607
|
+
const api = () => requireClient();
|
|
569
608
|
return Object.freeze({
|
|
570
609
|
// QQ 群(group_openid / member_openid):官方 v2 群管理接口,部分为白名单能力(11253)。
|
|
571
|
-
inspectGroupMember:
|
|
572
|
-
inspectGroupScene:
|
|
573
|
-
inspectGroupMemberProfile:
|
|
574
|
-
moderateGroupMember:
|
|
575
|
-
async listGroupMembers(groupId) {
|
|
576
|
-
|
|
610
|
+
inspectGroupMember: request => inspectQqGroupMember(api(), request),
|
|
611
|
+
inspectGroupScene: request => inspectQqGroupScene(api(), request),
|
|
612
|
+
inspectGroupMemberProfile: request => inspectQqGroupMemberProfile(api(), request),
|
|
613
|
+
moderateGroupMember: request => moderateQqGroupMember({ api: api(), isActive }, request),
|
|
614
|
+
async listGroupMembers({ groupId, signal }) {
|
|
615
|
+
const operation = createEndpointOperation({ signal });
|
|
616
|
+
return operation.run(() => qqGroupService(api()).getMembers(groupId));
|
|
577
617
|
},
|
|
578
|
-
async kickGroupMember(groupId, userId) {
|
|
579
|
-
await kickQqGroupMember(api(), groupId, userId);
|
|
618
|
+
async kickGroupMember({ groupId, userId, signal }) {
|
|
619
|
+
await kickQqGroupMember(api(), { groupId, userId, signal });
|
|
580
620
|
},
|
|
581
|
-
async muteGroupMember(groupId, userId, durationSeconds) {
|
|
582
|
-
await muteQqGroupMember(api(), groupId, userId, durationSeconds);
|
|
621
|
+
async muteGroupMember({ groupId, userId, durationSeconds, signal }) {
|
|
622
|
+
await muteQqGroupMember(api(), { groupId, userId, durationSeconds, signal });
|
|
583
623
|
},
|
|
584
|
-
async listChannels() {
|
|
624
|
+
async listChannels(request = {}) {
|
|
625
|
+
const operation = createEndpointOperation(request);
|
|
585
626
|
const channels = [];
|
|
586
627
|
const client = requireClient();
|
|
587
|
-
const guilds = await client.
|
|
628
|
+
const guilds = await operation.run(() => client.guildService.getList());
|
|
588
629
|
for (const guildValue of Array.isArray(guilds) ? guilds : []) {
|
|
589
630
|
const guild = asRecord(guildValue);
|
|
590
631
|
const guildId = String(guild.id ?? guild.guild_id ?? guildValue ?? '');
|
|
591
632
|
if (!guildId)
|
|
592
633
|
continue;
|
|
593
634
|
const guildName = String(guild.name ?? guild.guild_name ?? guildId);
|
|
594
|
-
const rows = await client.
|
|
635
|
+
const rows = await operation.run(() => client.channelService.getList(guildId));
|
|
595
636
|
for (const channelValue of Array.isArray(rows) ? rows : []) {
|
|
596
637
|
const channel = asRecord(channelValue);
|
|
597
638
|
const id = String(channel.id ?? channel.channel_id ?? channelValue ?? '');
|
|
@@ -620,24 +661,24 @@ function asRecord(value) {
|
|
|
620
661
|
*/
|
|
621
662
|
function createQqEndpointControl(requireBot, recallById) {
|
|
622
663
|
return Object.freeze({
|
|
623
|
-
recall: async (message) => {
|
|
664
|
+
recall: async ({ message, signal }) => {
|
|
624
665
|
if (isCompoundMessageId(message.id))
|
|
625
|
-
return recallById(message.id);
|
|
666
|
+
return createEndpointOperation({ signal }).run(() => recallById(message.id));
|
|
626
667
|
const kind = qqOutboundKind(message.conversation);
|
|
627
|
-
await recallQqMessage(requireBot(), kind, message.conversation.id, message.id);
|
|
668
|
+
await createEndpointOperation({ signal }).run(() => recallQqMessage(requireBot(), { kind, channelId: message.conversation.id, messageId: message.id }));
|
|
628
669
|
},
|
|
629
|
-
addReaction: async (message, emoji) => {
|
|
670
|
+
addReaction: async ({ message, emoji, signal }) => {
|
|
630
671
|
const target = qqGuildReactionTarget(message, emoji);
|
|
631
672
|
if (!target)
|
|
632
673
|
return null;
|
|
633
|
-
await requireBot().
|
|
674
|
+
await createEndpointOperation({ signal }).run(() => requireBot().reactionService.addGuildMessageReaction(target.channelId, target.messageId, target.type, target.id));
|
|
634
675
|
return `${target.type}:${target.id}`;
|
|
635
676
|
},
|
|
636
|
-
removeReaction: async (message, reactionId) => {
|
|
677
|
+
removeReaction: async ({ message, reactionId, signal }) => {
|
|
637
678
|
const target = qqGuildReactionTarget(message, reactionId);
|
|
638
679
|
if (!target)
|
|
639
680
|
return;
|
|
640
|
-
await requireBot().
|
|
681
|
+
await createEndpointOperation({ signal }).run(() => requireBot().reactionService.deleteGuildMessageReaction(target.channelId, target.messageId, target.type, target.id));
|
|
641
682
|
},
|
|
642
683
|
});
|
|
643
684
|
}
|
|
@@ -672,7 +713,7 @@ function qqGuildReactionTarget(message, emoji) {
|
|
|
672
713
|
const parsed = parseQqEmoji(emoji);
|
|
673
714
|
return parsed ? { channelId, messageId, ...parsed } : null;
|
|
674
715
|
}
|
|
675
|
-
async function recallQqMessage(bot, kind, channelId, messageId) {
|
|
716
|
+
async function recallQqMessage(bot, { kind, channelId, messageId }) {
|
|
676
717
|
const recall = { private: bot.recallPrivateMessage, group: bot.recallGroupMessage,
|
|
677
718
|
channel: bot.recallGuildMessage, direct: bot.recallDirectMessage }[kind];
|
|
678
719
|
if (!recall)
|
|
@@ -681,3 +722,15 @@ async function recallQqMessage(bot, kind, channelId, messageId) {
|
|
|
681
722
|
throw new EndpointDeliveryError('invalid_message_id', 'QQ recall requires a nonempty message ID', 'not_sent');
|
|
682
723
|
await recall.call(bot, channelId, messageId);
|
|
683
724
|
}
|
|
725
|
+
/** Logical cancellation only; the native SDK still owns its already-started ACK. */
|
|
726
|
+
function awaitOwnedAck(operation, signal) {
|
|
727
|
+
if (!signal)
|
|
728
|
+
return operation;
|
|
729
|
+
return new Promise((resolve, reject) => {
|
|
730
|
+
const abort = () => reject(signal.reason);
|
|
731
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
732
|
+
void operation.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
733
|
+
if (signal.aborted)
|
|
734
|
+
abort();
|
|
735
|
+
});
|
|
736
|
+
}
|