@zhin.js/adapter-qq 1.1.18 → 1.1.20
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 +46 -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/src/endpoint.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Bot, ReceiverMode } 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';
|
|
@@ -17,13 +18,23 @@ import {
|
|
|
17
18
|
* QQ endpoints — lifecycle, outbound, admit, agent tool surface.
|
|
18
19
|
*/
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
+
createEndpointSpecification,
|
|
22
|
+
endpointSpec,
|
|
23
|
+
type Endpoint,
|
|
24
|
+
type EndpointSpecification,
|
|
25
|
+
createEndpointLifecycle,
|
|
26
|
+
type EndpointLifecycle,
|
|
21
27
|
type EndpointControl,
|
|
22
28
|
type EndpointChannel,
|
|
23
29
|
type EndpointManagement,
|
|
24
30
|
type EndpointSendRequest,
|
|
25
31
|
type EndpointTransportState,
|
|
26
32
|
type AdapterOutageNotifier,
|
|
33
|
+
createEndpointOperation,
|
|
34
|
+
type EndpointGroupRequest,
|
|
35
|
+
type EndpointMemberRequest,
|
|
36
|
+
type EndpointMuteRequest,
|
|
37
|
+
type EndpointManagementRequest,
|
|
27
38
|
} from 'zhin.js/adapter';
|
|
28
39
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
29
40
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
@@ -43,16 +54,14 @@ import {
|
|
|
43
54
|
type ResolvedQqWebsocketConfig,
|
|
44
55
|
} from './protocol.js';
|
|
45
56
|
import {
|
|
46
|
-
|
|
57
|
+
qqHttpBotOptions,
|
|
47
58
|
registerQqWebhookRoutes,
|
|
48
|
-
type CreateQqHttpBot,
|
|
49
59
|
type QqHttpBotTransport,
|
|
50
60
|
} from './webhook.js';
|
|
51
61
|
import {
|
|
52
62
|
bindQqBotInboundEvents,
|
|
53
|
-
|
|
63
|
+
qqWebsocketBotOptions, configureQqWebsocketReceiver, disposeQqAuthorization,
|
|
54
64
|
normalizeQqMessage,
|
|
55
|
-
type CreateQqBot,
|
|
56
65
|
type QqBotTransport,
|
|
57
66
|
} from './ws.js';
|
|
58
67
|
import {
|
|
@@ -61,24 +70,26 @@ import {
|
|
|
61
70
|
type QqSideEventCaller,
|
|
62
71
|
} from './side-event-dispatch.js';
|
|
63
72
|
|
|
64
|
-
export type {
|
|
65
|
-
export type {
|
|
73
|
+
export type { QqBotTransport, QqOfficialApi } from './ws.js';
|
|
74
|
+
export type { QqHttpBotTransport } from './webhook.js';
|
|
66
75
|
|
|
67
76
|
export interface QqEndpointOptions {
|
|
68
77
|
readonly id: CapabilityId;
|
|
69
78
|
readonly config: ResolvedQqWebsocketConfig;
|
|
70
|
-
readonly createBot?: CreateQqBot;
|
|
71
79
|
/** Out-of-band outage notifier; absent in tests/hosts without outbound. */
|
|
72
80
|
readonly notifier?: AdapterOutageNotifier;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
export class QqWebsocketEndpoint extends
|
|
83
|
+
export class QqWebsocketEndpoint extends Bot<ReceiverMode.WEBSOCKET> implements Endpoint {
|
|
84
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
85
|
+
readonly #receiverResources: ReturnType<typeof configureQqWebsocketReceiver>;
|
|
76
86
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
77
87
|
|
|
78
88
|
readonly #options: QqEndpointOptions;
|
|
79
|
-
|
|
80
|
-
#
|
|
89
|
+
#connection?: AbortController;
|
|
90
|
+
#stopping?: Promise<void>;
|
|
81
91
|
#open = false;
|
|
92
|
+
#executionSignal?: AbortSignal;
|
|
82
93
|
#offlineNoticeAt = 0;
|
|
83
94
|
readonly #seenInteractions = new Map<string, number>();
|
|
84
95
|
/** Recent msg_id / event_id per conversation, so replies go out as passive messages. */
|
|
@@ -86,19 +97,22 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
86
97
|
readonly #activeGate = new QqActiveMessageGate();
|
|
87
98
|
readonly #lifecycle: EndpointLifecycle;
|
|
88
99
|
#cleanup?: () => Promise<void>;
|
|
89
|
-
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#
|
|
90
|
-
readonly control: EndpointControl = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
100
|
+
readonly #management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#connection !== undefined);
|
|
101
|
+
readonly #control: EndpointControl = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
91
102
|
|
|
92
103
|
constructor(options: QqEndpointOptions) {
|
|
93
|
-
super();
|
|
104
|
+
super(qqWebsocketBotOptions(options.config));
|
|
94
105
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
95
106
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id });
|
|
96
107
|
this.#options = options;
|
|
97
|
-
this.#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
this.#receiverResources = configureQqWebsocketReceiver(this, options.config);
|
|
109
|
+
const endpoint = this;
|
|
110
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
111
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
112
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
113
|
+
get transportState() { return endpoint.#transportState; }, management: this.#management, control: this.#control,
|
|
114
|
+
});
|
|
115
|
+
this.#bindBot(this);
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
|
|
@@ -106,46 +120,48 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
106
120
|
return this.#options.config.id;
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
get transportState(): EndpointTransportState {
|
|
123
|
+
get #transportState(): EndpointTransportState {
|
|
110
124
|
if (this.#lifecycle.state !== 'open') return this.#lifecycle.state;
|
|
111
|
-
return this.#
|
|
125
|
+
return this.#receiverResources.state();
|
|
112
126
|
}
|
|
113
127
|
|
|
114
|
-
async
|
|
128
|
+
async #startFramework(executionSignal: AbortSignal): Promise<void> {
|
|
129
|
+
this.#executionSignal = executionSignal;
|
|
115
130
|
await this.#lifecycle.start(async (handle) => {
|
|
116
|
-
const bot = this
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
const bot = this;
|
|
132
|
+
const previousCleanup = this.#cleanup;
|
|
133
|
+
await previousCleanup?.();
|
|
134
|
+
const releaseUploadDiagnostics = installQqUploadDiagnostics(bot, metadata => this.#logger.debug(formatCompact(metadata)));
|
|
119
135
|
let cancelled = false;
|
|
120
136
|
const startupAbort = new AbortController();
|
|
137
|
+
this.#connection = startupAbort;
|
|
138
|
+
let nativeStart: Promise<unknown> | undefined;
|
|
121
139
|
let stopping: Promise<void> | undefined;
|
|
140
|
+
let transportClose: (() => void) | undefined;
|
|
122
141
|
const stopBot = async () => {
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
await super.stop();
|
|
143
|
+
await nativeStart?.catch(() => undefined);
|
|
125
144
|
};
|
|
126
145
|
const cleanup = () => {
|
|
127
146
|
cancelled = true;
|
|
128
147
|
releaseUploadDiagnostics();
|
|
129
148
|
startupAbort.abort();
|
|
130
|
-
if (
|
|
149
|
+
if (transportClose) this.receiver.off('close', transportClose);
|
|
150
|
+
if (this.#connection === startupAbort) this.#connection = undefined;
|
|
131
151
|
return stopping ??= stopBot();
|
|
132
152
|
};
|
|
133
153
|
this.#cleanup = cleanup;
|
|
134
|
-
handle.onForceClose(() => { void cleanup(); });
|
|
154
|
+
handle.onForceClose(() => { void cleanup().catch(() => undefined); });
|
|
135
155
|
try {
|
|
136
|
-
|
|
137
|
-
if (cancelled || this.#
|
|
156
|
+
transportClose = () => {
|
|
157
|
+
if (cancelled || this.#connection === undefined) return;
|
|
138
158
|
this.#notifyOffline('transport_close');
|
|
139
|
-
void cleanup();
|
|
159
|
+
void cleanup().catch(() => undefined);
|
|
140
160
|
handle.notifyClosed();
|
|
141
|
-
}
|
|
142
|
-
this
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}, async (error) => {
|
|
146
|
-
if (cancelled) await stopBot();
|
|
147
|
-
throw error;
|
|
148
|
-
}), startupAbort.signal);
|
|
161
|
+
};
|
|
162
|
+
this.receiver.on('close', transportClose);
|
|
163
|
+
nativeStart = super.start();
|
|
164
|
+
await awaitQqStartup(nativeStart.then(() => undefined), startupAbort.signal);
|
|
149
165
|
if (cancelled) {
|
|
150
166
|
// SDK startup may create resources after stop; close this exact old client again.
|
|
151
167
|
await stopping;
|
|
@@ -167,27 +183,31 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
167
183
|
});
|
|
168
184
|
}
|
|
169
185
|
|
|
170
|
-
|
|
186
|
+
#openFramework(): void {
|
|
171
187
|
this.#open = true;
|
|
172
188
|
}
|
|
173
189
|
|
|
174
|
-
|
|
190
|
+
#closeFramework(): void {
|
|
175
191
|
this.#open = false;
|
|
176
192
|
}
|
|
177
193
|
|
|
178
|
-
async
|
|
179
|
-
this.#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
async #stopFramework(): Promise<void> {
|
|
195
|
+
this.#stopping ??= Promise.resolve().then(async () => {
|
|
196
|
+
this.#open = false;
|
|
197
|
+
const cleanup = this.#cleanup;
|
|
198
|
+
this.#cleanup = undefined;
|
|
199
|
+
try { await this.#lifecycle.stop(); await cleanup?.(); }
|
|
200
|
+
finally { this.removeAllListeners(undefined as never); this.#receiverResources.dispose(); disposeQqAuthorization(this); }
|
|
201
|
+
});
|
|
202
|
+
await this.#stopping;
|
|
184
203
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
async
|
|
206
|
+
async #sendFramework({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
188
207
|
// 媒体与 markdown/按钮不能同条发送(msg_type 7 与 2 互斥):公网图片嵌入 markdown,
|
|
189
208
|
// 拿不到 URL/尺寸的媒体降级为独立消息先发
|
|
190
|
-
const
|
|
209
|
+
const operation = createEndpointOperation({ signal });
|
|
210
|
+
const messages = formatOutboundMessages(payload, { imageSizes: await operation.prepare(() => collectOutboundImageSizes(payload, { signal })) });
|
|
191
211
|
const body = messages[messages.length - 1]!;
|
|
192
212
|
const kind = qqOutboundKind(conversation);
|
|
193
213
|
const bot = this.#requireBot();
|
|
@@ -198,7 +218,7 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
198
218
|
ledger: this.#passiveReplies,
|
|
199
219
|
activeGate: this.#activeGate,
|
|
200
220
|
log: fields => this.#logger.warn(formatCompact(fields)),
|
|
201
|
-
}, conversation, messages);
|
|
221
|
+
}, { conversation, messages, signal });
|
|
202
222
|
} catch (error) {
|
|
203
223
|
const { failure, diagnostic } = qqDeliveryFailure(error);
|
|
204
224
|
this.#logger.warn(formatCompact(diagnostic));
|
|
@@ -216,7 +236,7 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
216
236
|
async recallMessage(messageId: string): Promise<void> {
|
|
217
237
|
const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
|
|
218
238
|
const bot = this.#requireBot();
|
|
219
|
-
await recallQqMessage(bot, kind, channelId, qqMsgId);
|
|
239
|
+
await recallQqMessage(bot, { kind, channelId, messageId: qqMsgId });
|
|
220
240
|
this.#logger.debug(formatCompact({
|
|
221
241
|
op: 'qq_recall',
|
|
222
242
|
endpoint: this.#options.config.id,
|
|
@@ -238,8 +258,10 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
238
258
|
);
|
|
239
259
|
const roleProjection = projectQqGuildSenderRoles(msg.authorRoles);
|
|
240
260
|
if (msg.id) this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { id: msg.id });
|
|
241
|
-
void this.emit('message.receive', {
|
|
261
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
242
262
|
conversation,
|
|
263
|
+
event: msg.event,
|
|
264
|
+
platformSentAt: msg.platformSentAt,
|
|
243
265
|
message: { conversation, id: msg.id },
|
|
244
266
|
content,
|
|
245
267
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
@@ -278,12 +300,15 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
278
300
|
this.#seenInteractions.set(action.message.id, now);
|
|
279
301
|
if (this.#seenInteractions.size > 10_000) this.#seenInteractions.delete(this.#seenInteractions.keys().next().value!);
|
|
280
302
|
// ACK owns only platform loading state, never implies business acceptance.
|
|
281
|
-
void
|
|
303
|
+
void this[endpointSpec].events.run(() => awaitOwnedAck(action.ack(), this.#executionSignal)).then(
|
|
304
|
+
ok => { if (ok === false) this.#logger.warn('qq_interaction_ack_failed'); },
|
|
305
|
+
() => { if (this.#executionSignal?.aborted) this.#logger.debug('QQ ACK wait cancelled'); else this.#logger.warn('qq_interaction_ack_failed'); },
|
|
306
|
+
);
|
|
282
307
|
const conversation = qqInboundConversation(String(this.#options.id), action.message);
|
|
283
308
|
// INTERACTION_CREATE's id is an event_id the reply may answer passively.
|
|
284
309
|
this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { event_id: action.message.id });
|
|
285
|
-
void this.emit('message.receive', {
|
|
286
|
-
conversation, message: { conversation, id: action.message.id }, content: '',
|
|
310
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
311
|
+
event: action.message.event, conversation, message: { conversation, id: action.message.id }, content: '',
|
|
287
312
|
segments: action.message.segments, sender: { id: action.message.authorId }, endpointId: this.#options.config.id,
|
|
288
313
|
metadata: Object.freeze({ eventType: 'INTERACTION_CREATE', sourceMessageIdAvailable: action.sourceMessageId !== undefined,
|
|
289
314
|
...(action.sourceMessageId ? { sourceMessageId: action.sourceMessageId } : {}),
|
|
@@ -312,28 +337,32 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
312
337
|
|
|
313
338
|
#bindBot(bot: QqBotTransport): void {
|
|
314
339
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
340
|
+
void this[endpointSpec].events.run(() => {
|
|
341
|
+
if (this.#connection === undefined) return;
|
|
342
|
+
this.#emitPlatformEvent('message', raw);
|
|
343
|
+
const msg = normalizeQqMessage(raw);
|
|
344
|
+
if (msg) this.admit(msg);
|
|
345
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
319
346
|
});
|
|
320
347
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
348
|
+
void this[endpointSpec].events.run(() => {
|
|
349
|
+
if (this.#connection === undefined) return;
|
|
350
|
+
this.#emitPlatformEvent(eventName, raw);
|
|
351
|
+
if (this.admitInteraction(raw)) return;
|
|
352
|
+
receiveQqSideEvent(
|
|
353
|
+
(name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
354
|
+
this.#options.config.id,
|
|
355
|
+
bot,
|
|
356
|
+
eventName,
|
|
357
|
+
raw,
|
|
358
|
+
this.#logger,
|
|
359
|
+
);
|
|
360
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
332
361
|
});
|
|
333
362
|
}
|
|
334
363
|
|
|
335
364
|
#emitPlatformEvent(name: string, event: unknown): void {
|
|
336
|
-
void this.
|
|
365
|
+
void this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
337
366
|
this.#logger.warn(formatCompact({
|
|
338
367
|
op: 'qq_platform_event_failed',
|
|
339
368
|
event: name,
|
|
@@ -343,8 +372,8 @@ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
|
|
|
343
372
|
}
|
|
344
373
|
|
|
345
374
|
#requireBot(): QqBotTransport {
|
|
346
|
-
if (!this.#
|
|
347
|
-
return this
|
|
375
|
+
if (!this.#connection) throw new EndpointDeliveryError('endpoint_disconnected', 'QQ bot not connected', 'not_sent');
|
|
376
|
+
return this;
|
|
348
377
|
}
|
|
349
378
|
}
|
|
350
379
|
|
|
@@ -352,19 +381,20 @@ export interface QqHttpEndpointOptions {
|
|
|
352
381
|
readonly id: CapabilityId;
|
|
353
382
|
readonly http: HttpHost;
|
|
354
383
|
readonly config: ResolvedQqHttpConfig;
|
|
355
|
-
readonly createBot?: CreateQqHttpBot;
|
|
356
384
|
/** Out-of-band outage notifier; absent in tests/hosts without outbound. */
|
|
357
385
|
readonly notifier?: AdapterOutageNotifier;
|
|
358
386
|
}
|
|
359
387
|
|
|
360
388
|
/** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
|
|
361
|
-
export class QqHttpEndpoint extends
|
|
389
|
+
export class QqHttpEndpoint extends Bot<ReceiverMode.MIDDLEWARE, 'koa'> implements Endpoint {
|
|
390
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
362
391
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
363
392
|
|
|
364
393
|
readonly #options: QqHttpEndpointOptions;
|
|
365
|
-
|
|
366
|
-
#
|
|
394
|
+
#connection?: AbortController;
|
|
395
|
+
#stopping?: Promise<void>;
|
|
367
396
|
#open = false;
|
|
397
|
+
#executionSignal?: AbortSignal;
|
|
368
398
|
#offlineNoticeAt = 0;
|
|
369
399
|
readonly #seenInteractions = new Map<string, number>();
|
|
370
400
|
/** Recent msg_id / event_id per conversation, so replies go out as passive messages. */
|
|
@@ -372,19 +402,22 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
372
402
|
readonly #activeGate = new QqActiveMessageGate();
|
|
373
403
|
readonly #lifecycle: EndpointLifecycle;
|
|
374
404
|
#cleanup?: () => Promise<void>;
|
|
375
|
-
readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#
|
|
376
|
-
readonly control: EndpointControl = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
405
|
+
readonly #management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot(), () => this.#open && this.#connection !== undefined);
|
|
406
|
+
readonly #control: EndpointControl = createQqEndpointControl(() => this.#requireBot(), (id) => this.recallMessage(id));
|
|
377
407
|
|
|
378
408
|
constructor(options: QqHttpEndpointOptions) {
|
|
379
|
-
super();
|
|
409
|
+
super(qqHttpBotOptions(options.config));
|
|
380
410
|
this.#logger = getAdapterLogger('qq', options.config.id);
|
|
381
411
|
this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
|
|
382
412
|
this.#options = options;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
413
|
+
const endpoint = this;
|
|
414
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
415
|
+
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
416
|
+
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
417
|
+
get transportState() { return endpoint.#transportState; },
|
|
418
|
+
management: this.#management, control: this.#control,
|
|
419
|
+
});
|
|
420
|
+
this.#bindBot(this);
|
|
388
421
|
}
|
|
389
422
|
|
|
390
423
|
/** Live endpoint 名(Console/AdapterIndex 展示用)。 */
|
|
@@ -392,45 +425,44 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
392
425
|
return this.#options.config.id;
|
|
393
426
|
}
|
|
394
427
|
|
|
395
|
-
get transportState(): EndpointTransportState {
|
|
428
|
+
get #transportState(): EndpointTransportState {
|
|
396
429
|
return this.#lifecycle.state;
|
|
397
430
|
}
|
|
398
431
|
|
|
399
|
-
async
|
|
432
|
+
async #startFramework(executionSignal: AbortSignal): Promise<void> {
|
|
433
|
+
this.#executionSignal = executionSignal;
|
|
400
434
|
await this.#lifecycle.start(async (handle) => {
|
|
401
|
-
const bot = this
|
|
402
|
-
|
|
403
|
-
|
|
435
|
+
const bot = this;
|
|
436
|
+
const previousCleanup = this.#cleanup;
|
|
437
|
+
await previousCleanup?.();
|
|
438
|
+
const releaseUploadDiagnostics = installQqUploadDiagnostics(bot, metadata => this.#logger.debug(formatCompact(metadata)));
|
|
404
439
|
let cancelled = false;
|
|
405
440
|
const startupAbort = new AbortController();
|
|
441
|
+
this.#connection = startupAbort;
|
|
442
|
+
let nativeStart: Promise<unknown> | undefined;
|
|
406
443
|
let stopping: Promise<void> | undefined;
|
|
407
444
|
const routes: Array<() => void> = [];
|
|
408
445
|
const stopBot = async () => {
|
|
409
|
-
|
|
410
|
-
|
|
446
|
+
await super.stop();
|
|
447
|
+
await nativeStart?.catch(() => undefined);
|
|
411
448
|
};
|
|
412
449
|
const cleanup = () => {
|
|
413
450
|
cancelled = true;
|
|
414
451
|
releaseUploadDiagnostics();
|
|
415
452
|
startupAbort.abort();
|
|
416
|
-
if (this.#
|
|
453
|
+
if (this.#connection === startupAbort) this.#connection = undefined;
|
|
417
454
|
for (const release of routes.splice(0)) release();
|
|
418
455
|
return stopping ??= stopBot();
|
|
419
456
|
};
|
|
420
457
|
this.#cleanup = cleanup;
|
|
421
|
-
handle.onForceClose(() => { void cleanup(); });
|
|
458
|
+
handle.onForceClose(() => { void cleanup().catch(() => undefined); });
|
|
422
459
|
try {
|
|
423
|
-
this.#bindBot(bot);
|
|
424
460
|
routes.push(...registerQqWebhookRoutes(this.#options.http, {
|
|
425
461
|
config: this.#options.config,
|
|
426
462
|
getBot: () => cancelled ? null : bot,
|
|
427
463
|
}));
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}, async (error) => {
|
|
431
|
-
if (cancelled) await stopBot();
|
|
432
|
-
throw error;
|
|
433
|
-
}), startupAbort.signal);
|
|
464
|
+
nativeStart = super.start();
|
|
465
|
+
await awaitQqStartup(nativeStart.then(() => undefined), startupAbort.signal);
|
|
434
466
|
if (cancelled) {
|
|
435
467
|
// SDK startup may create resources after stop; close this exact old client again.
|
|
436
468
|
await stopping;
|
|
@@ -448,27 +480,31 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
448
480
|
});
|
|
449
481
|
}
|
|
450
482
|
|
|
451
|
-
|
|
483
|
+
#openFramework(): void {
|
|
452
484
|
this.#open = true;
|
|
453
485
|
}
|
|
454
486
|
|
|
455
|
-
|
|
487
|
+
#closeFramework(): void {
|
|
456
488
|
this.#open = false;
|
|
457
489
|
}
|
|
458
490
|
|
|
459
|
-
async
|
|
460
|
-
this.#
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
491
|
+
async #stopFramework(): Promise<void> {
|
|
492
|
+
this.#stopping ??= Promise.resolve().then(async () => {
|
|
493
|
+
this.#open = false;
|
|
494
|
+
const cleanup = this.#cleanup;
|
|
495
|
+
this.#cleanup = undefined;
|
|
496
|
+
try { await this.#lifecycle.stop(); await cleanup?.(); }
|
|
497
|
+
finally { this.removeAllListeners(undefined as never); disposeQqAuthorization(this); }
|
|
498
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
499
|
+
});
|
|
500
|
+
await this.#stopping;
|
|
466
501
|
}
|
|
467
502
|
|
|
468
|
-
async
|
|
503
|
+
async #sendFramework({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
469
504
|
// 媒体与 markdown/按钮不能同条发送(msg_type 7 与 2 互斥):公网图片嵌入 markdown,
|
|
470
505
|
// 拿不到 URL/尺寸的媒体降级为独立消息先发
|
|
471
|
-
const
|
|
506
|
+
const operation = createEndpointOperation({ signal });
|
|
507
|
+
const messages = formatOutboundMessages(payload, { imageSizes: await operation.prepare(() => collectOutboundImageSizes(payload, { signal })) });
|
|
472
508
|
const body = messages[messages.length - 1]!;
|
|
473
509
|
const kind = qqOutboundKind(conversation);
|
|
474
510
|
const bot = this.#requireBot();
|
|
@@ -479,7 +515,7 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
479
515
|
ledger: this.#passiveReplies,
|
|
480
516
|
activeGate: this.#activeGate,
|
|
481
517
|
log: fields => this.#logger.warn(formatCompact(fields)),
|
|
482
|
-
}, conversation, messages);
|
|
518
|
+
}, { conversation, messages, signal });
|
|
483
519
|
} catch (error) {
|
|
484
520
|
const { failure, diagnostic } = qqDeliveryFailure(error);
|
|
485
521
|
this.#logger.warn(formatCompact(diagnostic));
|
|
@@ -497,7 +533,7 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
497
533
|
async recallMessage(messageId: string): Promise<void> {
|
|
498
534
|
const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
|
|
499
535
|
const bot = this.#requireBot();
|
|
500
|
-
await recallQqMessage(bot, kind, channelId, qqMsgId);
|
|
536
|
+
await recallQqMessage(bot, { kind, channelId, messageId: qqMsgId });
|
|
501
537
|
this.#logger.debug(formatCompact({
|
|
502
538
|
op: 'qq_recall',
|
|
503
539
|
endpoint: this.#options.config.id,
|
|
@@ -518,8 +554,10 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
518
554
|
);
|
|
519
555
|
const roleProjection = projectQqGuildSenderRoles(msg.authorRoles);
|
|
520
556
|
if (msg.id) this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { id: msg.id });
|
|
521
|
-
void this.emit('message.receive', {
|
|
557
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
522
558
|
conversation,
|
|
559
|
+
event: msg.event,
|
|
560
|
+
platformSentAt: msg.platformSentAt,
|
|
523
561
|
message: { conversation, id: msg.id },
|
|
524
562
|
content,
|
|
525
563
|
...(msg.segments?.length ? { segments: msg.segments } : {}),
|
|
@@ -558,12 +596,15 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
558
596
|
this.#seenInteractions.set(action.message.id, now);
|
|
559
597
|
if (this.#seenInteractions.size > 10_000) this.#seenInteractions.delete(this.#seenInteractions.keys().next().value!);
|
|
560
598
|
// ACK owns only platform loading state, never implies business acceptance.
|
|
561
|
-
void
|
|
599
|
+
void this[endpointSpec].events.run(() => awaitOwnedAck(action.ack(), this.#executionSignal)).then(
|
|
600
|
+
ok => { if (ok === false) this.#logger.warn('qq_interaction_ack_failed'); },
|
|
601
|
+
() => { if (this.#executionSignal?.aborted) this.#logger.debug('QQ ACK wait cancelled'); else this.#logger.warn('qq_interaction_ack_failed'); },
|
|
602
|
+
);
|
|
562
603
|
const conversation = qqInboundConversation(String(this.#options.id), action.message);
|
|
563
604
|
// INTERACTION_CREATE's id is an event_id the reply may answer passively.
|
|
564
605
|
this.#passiveReplies.record(qqOutboundKind(conversation), conversation.id, { event_id: action.message.id });
|
|
565
|
-
void this.emit('message.receive', {
|
|
566
|
-
conversation, message: { conversation, id: action.message.id }, content: '',
|
|
606
|
+
void this[endpointSpec].events.emit('message.receive', {
|
|
607
|
+
event: action.message.event, conversation, message: { conversation, id: action.message.id }, content: '',
|
|
567
608
|
segments: action.message.segments, sender: { id: action.message.authorId }, endpointId: this.#options.config.id,
|
|
568
609
|
metadata: Object.freeze({ eventType: 'INTERACTION_CREATE', sourceMessageIdAvailable: action.sourceMessageId !== undefined,
|
|
569
610
|
...(action.sourceMessageId ? { sourceMessageId: action.sourceMessageId } : {}),
|
|
@@ -592,28 +633,32 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
592
633
|
|
|
593
634
|
#bindBot(bot: QqBotTransport): void {
|
|
594
635
|
bindQqBotInboundEvents(bot, (raw) => {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
636
|
+
void this[endpointSpec].events.run(() => {
|
|
637
|
+
if (this.#connection === undefined) return;
|
|
638
|
+
this.#emitPlatformEvent('message', raw);
|
|
639
|
+
const msg = normalizeQqMessage(raw);
|
|
640
|
+
if (msg) this.admit(msg);
|
|
641
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
599
642
|
});
|
|
600
643
|
bindQqBotSideEvents(bot, (eventName, raw) => {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
644
|
+
void this[endpointSpec].events.run(() => {
|
|
645
|
+
if (this.#connection === undefined) return;
|
|
646
|
+
this.#emitPlatformEvent(eventName, raw);
|
|
647
|
+
if (this.admitInteraction(raw)) return;
|
|
648
|
+
receiveQqSideEvent(
|
|
649
|
+
(name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
650
|
+
this.#options.config.id,
|
|
651
|
+
bot,
|
|
652
|
+
eventName,
|
|
653
|
+
raw,
|
|
654
|
+
this.#logger,
|
|
655
|
+
);
|
|
656
|
+
}).catch(() => this.#logger.debug('Native callback was not admitted'));
|
|
612
657
|
});
|
|
613
658
|
}
|
|
614
659
|
|
|
615
660
|
#emitPlatformEvent(name: string, event: unknown): void {
|
|
616
|
-
void this.
|
|
661
|
+
void this[endpointSpec].events.platform(name, event).catch((error) => {
|
|
617
662
|
this.#logger.warn(formatCompact({
|
|
618
663
|
op: 'qq_platform_event_failed',
|
|
619
664
|
event: name,
|
|
@@ -623,41 +668,43 @@ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
|
|
|
623
668
|
}
|
|
624
669
|
|
|
625
670
|
#requireBot(): QqHttpBotTransport {
|
|
626
|
-
if (!this.#
|
|
627
|
-
return this
|
|
671
|
+
if (!this.#connection) throw new EndpointDeliveryError('endpoint_disconnected', 'QQ bot not connected', 'not_sent');
|
|
672
|
+
return this;
|
|
628
673
|
}
|
|
629
674
|
}
|
|
630
675
|
|
|
631
676
|
function createQqEndpointManagement(
|
|
632
|
-
requireClient: () =>
|
|
677
|
+
requireClient: () => QqBotTransport,
|
|
633
678
|
isActive: () => boolean,
|
|
634
679
|
): EndpointManagement {
|
|
635
|
-
const api = () => requireClient()
|
|
680
|
+
const api = () => requireClient();
|
|
636
681
|
return Object.freeze<EndpointManagement>({
|
|
637
682
|
// QQ 群(group_openid / member_openid):官方 v2 群管理接口,部分为白名单能力(11253)。
|
|
638
|
-
inspectGroupMember:
|
|
639
|
-
inspectGroupScene:
|
|
640
|
-
inspectGroupMemberProfile:
|
|
641
|
-
moderateGroupMember:
|
|
642
|
-
async listGroupMembers(groupId:
|
|
643
|
-
|
|
683
|
+
inspectGroupMember: request => inspectQqGroupMember(api(), request),
|
|
684
|
+
inspectGroupScene: request => inspectQqGroupScene(api(), request),
|
|
685
|
+
inspectGroupMemberProfile: request => inspectQqGroupMemberProfile(api(), request),
|
|
686
|
+
moderateGroupMember: request => moderateQqGroupMember({ api: api(), isActive }, request),
|
|
687
|
+
async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly unknown[]> {
|
|
688
|
+
const operation = createEndpointOperation({ signal });
|
|
689
|
+
return operation.run(() => qqGroupService(api()).getMembers(groupId));
|
|
644
690
|
},
|
|
645
|
-
async kickGroupMember(groupId
|
|
646
|
-
await kickQqGroupMember(api(), groupId, userId);
|
|
691
|
+
async kickGroupMember({ groupId, userId, signal }: EndpointMemberRequest): Promise<void> {
|
|
692
|
+
await kickQqGroupMember(api(), { groupId, userId, signal });
|
|
647
693
|
},
|
|
648
|
-
async muteGroupMember(groupId
|
|
649
|
-
await muteQqGroupMember(api(), groupId, userId, durationSeconds);
|
|
694
|
+
async muteGroupMember({ groupId, userId, durationSeconds, signal }: EndpointMuteRequest): Promise<void> {
|
|
695
|
+
await muteQqGroupMember(api(), { groupId, userId, durationSeconds, signal });
|
|
650
696
|
},
|
|
651
|
-
async listChannels(): Promise<readonly EndpointChannel[]> {
|
|
697
|
+
async listChannels(request: EndpointManagementRequest = {}): Promise<readonly EndpointChannel[]> {
|
|
698
|
+
const operation = createEndpointOperation(request);
|
|
652
699
|
const channels: EndpointChannel[] = [];
|
|
653
700
|
const client = requireClient();
|
|
654
|
-
const guilds = await client.
|
|
701
|
+
const guilds = await operation.run(() => client.guildService.getList());
|
|
655
702
|
for (const guildValue of Array.isArray(guilds) ? guilds : []) {
|
|
656
703
|
const guild = asRecord(guildValue);
|
|
657
704
|
const guildId = String(guild.id ?? guild.guild_id ?? guildValue ?? '');
|
|
658
705
|
if (!guildId) continue;
|
|
659
706
|
const guildName = String(guild.name ?? guild.guild_name ?? guildId);
|
|
660
|
-
const rows = await client.
|
|
707
|
+
const rows = await operation.run(() => client.channelService.getList(guildId));
|
|
661
708
|
for (const channelValue of Array.isArray(rows) ? rows : []) {
|
|
662
709
|
const channel = asRecord(channelValue);
|
|
663
710
|
const id = String(channel.id ?? channel.channel_id ?? channelValue ?? '');
|
|
@@ -690,21 +737,21 @@ function createQqEndpointControl(
|
|
|
690
737
|
recallById: (messageId: string) => Promise<void>,
|
|
691
738
|
): EndpointControl {
|
|
692
739
|
return Object.freeze<EndpointControl>({
|
|
693
|
-
recall: async (message
|
|
694
|
-
if (isCompoundMessageId(message.id)) return recallById(message.id);
|
|
740
|
+
recall: async ({ message, signal }) => {
|
|
741
|
+
if (isCompoundMessageId(message.id)) return createEndpointOperation({ signal }).run(() => recallById(message.id));
|
|
695
742
|
const kind = qqOutboundKind(message.conversation);
|
|
696
|
-
await recallQqMessage(requireBot(), kind, message.conversation.id, message.id);
|
|
743
|
+
await createEndpointOperation({ signal }).run(() => recallQqMessage(requireBot(), { kind, channelId: message.conversation.id, messageId: message.id }));
|
|
697
744
|
},
|
|
698
|
-
addReaction: async (message
|
|
745
|
+
addReaction: async ({ message, emoji, signal }) => {
|
|
699
746
|
const target = qqGuildReactionTarget(message, emoji);
|
|
700
747
|
if (!target) return null;
|
|
701
|
-
await requireBot().
|
|
748
|
+
await createEndpointOperation({ signal }).run(() => requireBot().reactionService.addGuildMessageReaction(target.channelId, target.messageId, target.type, target.id));
|
|
702
749
|
return `${target.type}:${target.id}`;
|
|
703
750
|
},
|
|
704
|
-
removeReaction: async (message
|
|
751
|
+
removeReaction: async ({ message, reactionId, signal }) => {
|
|
705
752
|
const target = qqGuildReactionTarget(message, reactionId);
|
|
706
753
|
if (!target) return;
|
|
707
|
-
await requireBot().
|
|
754
|
+
await createEndpointOperation({ signal }).run(() => requireBot().reactionService.deleteGuildMessageReaction(target.channelId, target.messageId, target.type, target.id));
|
|
708
755
|
},
|
|
709
756
|
});
|
|
710
757
|
}
|
|
@@ -733,10 +780,21 @@ function qqGuildReactionTarget(message: MessageRef, emoji: string) {
|
|
|
733
780
|
return parsed ? { channelId, messageId, ...parsed } : null;
|
|
734
781
|
}
|
|
735
782
|
|
|
736
|
-
async function recallQqMessage(bot: QqBotTransport, kind: ReturnType<typeof qqOutboundKind
|
|
783
|
+
async function recallQqMessage(bot: QqBotTransport, { kind, channelId, messageId }: { readonly kind: ReturnType<typeof qqOutboundKind>; readonly channelId: string; readonly messageId: string }): Promise<void> {
|
|
737
784
|
const recall = { private: bot.recallPrivateMessage, group: bot.recallGroupMessage,
|
|
738
785
|
channel: bot.recallGuildMessage, direct: bot.recallDirectMessage }[kind];
|
|
739
786
|
if (!recall) throw new EndpointDeliveryError('recall_unsupported', 'QQ transport does not support recall for this conversation', 'not_sent');
|
|
740
787
|
if (!messageId) throw new EndpointDeliveryError('invalid_message_id', 'QQ recall requires a nonempty message ID', 'not_sent');
|
|
741
788
|
await recall.call(bot, channelId, messageId);
|
|
742
789
|
}
|
|
790
|
+
|
|
791
|
+
/** Logical cancellation only; the native SDK still owns its already-started ACK. */
|
|
792
|
+
function awaitOwnedAck<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
793
|
+
if (!signal) return operation;
|
|
794
|
+
return new Promise((resolve, reject) => {
|
|
795
|
+
const abort = () => reject(signal.reason);
|
|
796
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
797
|
+
void operation.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
798
|
+
if (signal.aborted) abort();
|
|
799
|
+
});
|
|
800
|
+
}
|