@zhin.js/adapter-icqq 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 +28 -0
- package/README.md +82 -3
- package/adapters/icqq.js +1 -3
- package/adapters/icqq.ts +2 -8
- package/agent/prompt-sections/platform.ts +17 -0
- package/lib/client.d.ts +19 -0
- package/lib/client.js +3 -0
- package/lib/endpoint.d.ts +9 -6
- package/lib/endpoint.js +232 -83
- package/lib/icqq-endpoint-commands.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/package.json +21 -14
- package/plugin.js +1 -24
- package/src/client.ts +29 -0
- package/src/endpoint.ts +236 -85
- package/src/index.ts +4 -6
- package/tools/announce.js +3 -6
- package/tools/announce.ts +4 -7
- package/tools/essence.js +5 -7
- package/tools/essence.ts +6 -8
- package/tools/friend_list.js +6 -9
- package/tools/friend_list.ts +7 -10
- package/tools/group_files.js +3 -6
- package/tools/group_files.ts +4 -7
- package/tools/group_invite.js +6 -8
- package/tools/group_invite.ts +7 -9
- package/tools/group_kick.js +6 -8
- package/tools/group_kick.ts +7 -9
- package/tools/group_list.js +7 -10
- package/tools/group_list.ts +8 -11
- package/tools/group_member_info.js +3 -6
- package/tools/group_member_info.ts +3 -7
- package/tools/group_sign.js +5 -7
- package/tools/group_sign.ts +6 -8
- package/tools/list_muted.js +3 -6
- package/tools/list_muted.ts +4 -7
- package/tools/poke.js +3 -6
- package/tools/poke.ts +4 -7
- package/tools/send_user_like.js +3 -6
- package/tools/send_user_like.ts +4 -7
- package/tools/set_anonymous.js +5 -7
- package/tools/set_anonymous.ts +6 -8
- package/tools/set_mute.js +6 -8
- package/tools/set_mute.ts +7 -9
- package/tools/set_title.js +6 -8
- package/tools/set_title.ts +7 -9
- package/tools/user_info.js +3 -6
- package/tools/user_info.ts +4 -7
- package/lib/agent-prompt.d.ts +0 -2
- package/lib/agent-prompt.js +0 -97
- package/lib/icqq-agent-deps.d.ts +0 -13
- package/lib/icqq-agent-deps.js +0 -27
- package/src/agent-prompt.ts +0 -110
- package/src/icqq-agent-deps.ts +0 -43
package/src/endpoint.ts
CHANGED
|
@@ -8,15 +8,15 @@ import {
|
|
|
8
8
|
type VideoElem,
|
|
9
9
|
} from '@icqqjs/icqq';
|
|
10
10
|
import { inspect } from 'node:util';
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
12
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
11
13
|
import type {
|
|
12
14
|
EndpointControl,
|
|
13
|
-
EndpointInstance,
|
|
14
15
|
EndpointManagement,
|
|
15
16
|
EndpointPendingRequest,
|
|
16
17
|
EndpointSendRequest,
|
|
17
18
|
} from 'zhin.js/adapter';
|
|
18
19
|
import type { EndpointContentPort, EndpointContentResolveContext } from '@zhin.js/adapter';
|
|
19
|
-
import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
|
|
20
20
|
import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments, type LoginAssist } from '@zhin.js/core';
|
|
21
21
|
import type {
|
|
22
22
|
ConversationMessage,
|
|
@@ -27,7 +27,6 @@ import type {
|
|
|
27
27
|
} from '@zhin.js/im-contract';
|
|
28
28
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
29
29
|
import type { CapabilityId } from 'zhin.js';
|
|
30
|
-
import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
|
|
31
30
|
import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
|
|
32
31
|
import {
|
|
33
32
|
InboundMessageDeduper,
|
|
@@ -66,9 +65,7 @@ import { normalizeForwardMsgResponse } from './forward-msg.js';
|
|
|
66
65
|
|
|
67
66
|
export interface IcqqEndpointOptions {
|
|
68
67
|
readonly id: CapabilityId;
|
|
69
|
-
readonly gateway: MessageGateway;
|
|
70
68
|
readonly config: ResolvedIcqqConfig;
|
|
71
|
-
readonly sideEvents: SideEventGateway;
|
|
72
69
|
readonly loginAssist: LoginAssist;
|
|
73
70
|
}
|
|
74
71
|
|
|
@@ -91,7 +88,16 @@ const BOUND_EVENTS = [
|
|
|
91
88
|
'system.login.auth',
|
|
92
89
|
] as const;
|
|
93
90
|
|
|
94
|
-
|
|
91
|
+
class ManagedIcqqClient extends Client {
|
|
92
|
+
cancelReconnect(): void {
|
|
93
|
+
if (!this.login_timer) return;
|
|
94
|
+
clearTimeout(this.login_timer);
|
|
95
|
+
this.login_timer = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class IcqqEndpoint extends Endpoint<Client> {
|
|
100
|
+
readonly client: ManagedIcqqClient;
|
|
95
101
|
readonly #logger: ReturnType<typeof getAdapterLogger>;
|
|
96
102
|
readonly #options: IcqqEndpointOptions;
|
|
97
103
|
readonly endpointName: string;
|
|
@@ -102,10 +108,13 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
102
108
|
#open = false;
|
|
103
109
|
#retiring = false;
|
|
104
110
|
#started = false;
|
|
111
|
+
#retirementEpoch = 0;
|
|
112
|
+
#retirementArmed = false;
|
|
113
|
+
#disposeNativeEvents?: () => void;
|
|
105
114
|
#resolveStartOnline?: () => void;
|
|
106
115
|
#heldInbound: IcqqInboundMessage[] = [];
|
|
107
|
-
#unregisterAgent?: () => void;
|
|
108
116
|
readonly #inflightInbound = new Set<Promise<void>>();
|
|
117
|
+
readonly #inboundOwner = new AsyncLocalStorage<symbol>();
|
|
109
118
|
readonly #messageContent = new Map<string, ConversationMessage>();
|
|
110
119
|
|
|
111
120
|
readonly content: EndpointContentPort = Object.freeze({
|
|
@@ -156,7 +165,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
156
165
|
// ICQQ does not expose a cancellable getForwardMsg API. Keep the caller's
|
|
157
166
|
// generation lease until the native operation actually settles, then honor
|
|
158
167
|
// cancellation before publishing any resolved content.
|
|
159
|
-
const raw = await this.getForwardMsg(forwardId);
|
|
168
|
+
const raw = await this.client.getForwardMsg(forwardId);
|
|
160
169
|
context.signal.throwIfAborted();
|
|
161
170
|
const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
|
|
162
171
|
state.remainingEntries -= entries.length;
|
|
@@ -190,43 +199,43 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
190
199
|
|
|
191
200
|
readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
192
201
|
listFriends: async () =>
|
|
193
|
-
Array.from(this.fl.values()).map((f) => ({
|
|
202
|
+
Array.from(this.client.fl.values()).map((f) => ({
|
|
194
203
|
user_id: f.user_id,
|
|
195
204
|
nickname: f.nickname,
|
|
196
205
|
remark: f.remark ?? '',
|
|
197
206
|
})),
|
|
198
207
|
listGroups: async () =>
|
|
199
|
-
Array.from(this.gl.values()).map((g) => ({
|
|
208
|
+
Array.from(this.client.gl.values()).map((g) => ({
|
|
200
209
|
group_id: g.group_id,
|
|
201
210
|
name: g.group_name,
|
|
202
211
|
})),
|
|
203
212
|
listChannels: async () => {
|
|
204
|
-
await this.#guildCatalog.syncAll(this);
|
|
213
|
+
await this.#guildCatalog.syncAll(this.client);
|
|
205
214
|
return this.#guildCatalog.getGuildChannelList();
|
|
206
215
|
},
|
|
207
216
|
listGroupMembers: async (groupId) =>
|
|
208
|
-
Array.from((await this.getGroupMemberList(Number(groupId))).values()),
|
|
217
|
+
Array.from((await this.client.getGroupMemberList(Number(groupId))).values()),
|
|
209
218
|
listRequests: () => this.#listPendingRequests(),
|
|
210
219
|
approveRequest: (requestId, remark) => this.#approveRequest(requestId, remark),
|
|
211
220
|
rejectRequest: (requestId, reason) => this.#rejectRequest(requestId, reason),
|
|
212
221
|
kickGroupMember: async (groupId, userId) => {
|
|
213
|
-
await this.setGroupKick(Number(groupId), Number(userId));
|
|
222
|
+
await this.client.setGroupKick(Number(groupId), Number(userId));
|
|
214
223
|
},
|
|
215
224
|
muteGroupMember: async (groupId, userId, duration) => {
|
|
216
|
-
await this.setGroupBan(Number(groupId), Number(userId), duration);
|
|
225
|
+
await this.client.setGroupBan(Number(groupId), Number(userId), duration);
|
|
217
226
|
},
|
|
218
227
|
setGroupAdmin: async (groupId, userId, enabled) => {
|
|
219
|
-
await this.setGroupAdmin(Number(groupId), Number(userId), enabled);
|
|
228
|
+
await this.client.setGroupAdmin(Number(groupId), Number(userId), enabled);
|
|
220
229
|
},
|
|
221
230
|
deleteFriend: async (userId) => {
|
|
222
|
-
await this.deleteFriend(Number(userId));
|
|
231
|
+
await this.client.deleteFriend(Number(userId));
|
|
223
232
|
},
|
|
224
233
|
});
|
|
225
234
|
|
|
226
235
|
readonly control: EndpointControl = Object.freeze({
|
|
227
236
|
recall: async (message: MessageRef) => {
|
|
228
237
|
if (!message.id || message.id.startsWith('outbound:')) return;
|
|
229
|
-
await this.deleteMsg(message.id);
|
|
238
|
+
await this.client.deleteMsg(message.id);
|
|
230
239
|
},
|
|
231
240
|
addReaction: async (message: MessageRef, emoji: string) => {
|
|
232
241
|
const target = resolveIcqqGroupReactionTarget(message);
|
|
@@ -235,7 +244,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
235
244
|
// Do not await protocol ACK — packet timeout must not stall the AI reply.
|
|
236
245
|
this.#ignoreReactionAck(
|
|
237
246
|
'add',
|
|
238
|
-
this.pickGroup(target.groupId).setReaction(target.seq, emoji),
|
|
247
|
+
this.client.pickGroup(target.groupId).setReaction(target.seq, emoji),
|
|
239
248
|
);
|
|
240
249
|
} catch (error) {
|
|
241
250
|
this.#logger.debug(formatCompact({
|
|
@@ -251,7 +260,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
251
260
|
try {
|
|
252
261
|
this.#ignoreReactionAck(
|
|
253
262
|
'remove',
|
|
254
|
-
this.pickGroup(target.groupId).delReaction(target.seq, reactionId),
|
|
263
|
+
this.client.pickGroup(target.groupId).delReaction(target.seq, reactionId),
|
|
255
264
|
);
|
|
256
265
|
} catch (error) {
|
|
257
266
|
this.#logger.debug(formatCompact({
|
|
@@ -263,8 +272,9 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
263
272
|
});
|
|
264
273
|
|
|
265
274
|
constructor(options: IcqqEndpointOptions) {
|
|
275
|
+
super();
|
|
266
276
|
const nativeConfig = resolveNativeClientConfig(options.config);
|
|
267
|
-
|
|
277
|
+
this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig);
|
|
268
278
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
269
279
|
this.#options = options;
|
|
270
280
|
this.endpointName = options.config.id;
|
|
@@ -282,6 +292,12 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
282
292
|
async start(signal: AbortSignal): Promise<void> {
|
|
283
293
|
if (this.#started) return;
|
|
284
294
|
signal.throwIfAborted();
|
|
295
|
+
// Invalidate a guard left by a prior lifecycle epoch before admitting a
|
|
296
|
+
// new login attempt on this endpoint instance.
|
|
297
|
+
this.#retirementEpoch += 1;
|
|
298
|
+
this.#retirementArmed = false;
|
|
299
|
+
this.#retiring = false;
|
|
300
|
+
this.client.off('system.online');
|
|
285
301
|
this.#started = true;
|
|
286
302
|
let resolveOnline!: () => void;
|
|
287
303
|
const onlineReady = new Promise<void>((resolve) => {
|
|
@@ -292,14 +308,13 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
292
308
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
293
309
|
try {
|
|
294
310
|
this.#bindClientEvents();
|
|
295
|
-
await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
|
|
311
|
+
await raceAbort(Promise.resolve(this.client.login(this.#options.config.password)), signal);
|
|
296
312
|
await onlineReady;
|
|
297
313
|
signal.throwIfAborted();
|
|
298
314
|
await this.#pullPendingSystemMessages(signal);
|
|
299
315
|
signal.throwIfAborted();
|
|
300
|
-
this.#unregisterAgent = registerIcqqAgentEndpoint(this.endpointName, this);
|
|
301
316
|
this.#logger.info(
|
|
302
|
-
`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`,
|
|
317
|
+
`connected (direct) | friends: ${this.client.fl.size} | groups: ${this.client.gl.size}`,
|
|
303
318
|
);
|
|
304
319
|
} catch (error) {
|
|
305
320
|
await this.stop();
|
|
@@ -312,33 +327,49 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
312
327
|
}
|
|
313
328
|
|
|
314
329
|
open(): void {
|
|
330
|
+
if (this.#retirementArmed) {
|
|
331
|
+
this.#retirementEpoch += 1;
|
|
332
|
+
this.#retirementArmed = false;
|
|
333
|
+
this.client.off('system.online');
|
|
334
|
+
this.#bindSystemOnlineEvent();
|
|
335
|
+
}
|
|
315
336
|
this.#retiring = false;
|
|
316
337
|
this.#open = true;
|
|
317
338
|
const held = this.#heldInbound.splice(0);
|
|
318
|
-
for (const msg of held)
|
|
339
|
+
for (const msg of held) {
|
|
340
|
+
void this.admit(msg).catch((error) => {
|
|
341
|
+
this.#logInboundHandlerFailure('held', error);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
319
344
|
}
|
|
320
345
|
|
|
321
346
|
async close(): Promise<void> {
|
|
322
|
-
this.#
|
|
323
|
-
this.#
|
|
347
|
+
this.#assertExternalLifecycle('close');
|
|
348
|
+
this.#armRetirementFence();
|
|
324
349
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
325
350
|
}
|
|
326
351
|
|
|
327
352
|
async stop(): Promise<void> {
|
|
328
|
-
this.#
|
|
329
|
-
this.#
|
|
353
|
+
this.#assertExternalLifecycle('stop');
|
|
354
|
+
this.#armRetirementFence();
|
|
330
355
|
await Promise.allSettled([...this.#inflightInbound]);
|
|
331
356
|
this.#resolveStartOnline?.();
|
|
332
357
|
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'endpoint_stopped');
|
|
333
358
|
this.#heldInbound.length = 0;
|
|
334
|
-
this.#
|
|
335
|
-
this.#
|
|
336
|
-
for (const event of BOUND_EVENTS)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
359
|
+
this.#disposeNativeEvents?.();
|
|
360
|
+
this.#disposeNativeEvents = undefined;
|
|
361
|
+
for (const event of BOUND_EVENTS) {
|
|
362
|
+
// The retirement guard owns this matcher until a late login handshake
|
|
363
|
+
// has finished. start()/open() replace it with the normal listener.
|
|
364
|
+
if (event !== 'system.online') this.client.off(event);
|
|
365
|
+
}
|
|
366
|
+
// `Client.logout()` sends an account-level unregister packet before it
|
|
367
|
+
// closes this transport. During config HMR the replacement endpoint for
|
|
368
|
+
// the same UIN is already online, so unregistering the retiring generation
|
|
369
|
+
// also closes the replacement session. Endpoint ownership is transport
|
|
370
|
+
// scoped: retire only this Client's TCP connection.
|
|
340
371
|
try {
|
|
341
|
-
this.terminate();
|
|
372
|
+
this.client.terminate();
|
|
342
373
|
} catch { /* ignore */ }
|
|
343
374
|
this.#inboundDeduper.clear();
|
|
344
375
|
this.#sideEventDeduper.clear();
|
|
@@ -347,6 +378,28 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
347
378
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
348
379
|
}
|
|
349
380
|
|
|
381
|
+
#armRetirementFence(): void {
|
|
382
|
+
this.#retiring = true;
|
|
383
|
+
this.#open = false;
|
|
384
|
+
if (this.#retirementArmed) return;
|
|
385
|
+
this.#retirementArmed = true;
|
|
386
|
+
const retirementEpoch = ++this.#retirementEpoch;
|
|
387
|
+
// ICQQ owns reconnect through this per-Client timer. Clear it before any
|
|
388
|
+
// asynchronous drain so a queued callback cannot enter while close waits.
|
|
389
|
+
this.client.cancelReconnect();
|
|
390
|
+
// A login callback may already be inside the SDK handshake, which has no
|
|
391
|
+
// abort API. Replace the normal online listener with an epoch-owned guard
|
|
392
|
+
// before draining inbound work. Transport termination is account-local;
|
|
393
|
+
// logout would also unregister an HMR replacement using the same UIN.
|
|
394
|
+
this.client.off('system.online');
|
|
395
|
+
this.client.on('system.online', () => {
|
|
396
|
+
if (this.#retirementEpoch !== retirementEpoch || !this.#retiring) return;
|
|
397
|
+
try {
|
|
398
|
+
this.client.terminate();
|
|
399
|
+
} catch { /* already closed */ }
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
350
403
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
351
404
|
if (!this.#started) throw new Error('icqq endpoint 未连接');
|
|
352
405
|
const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
|
|
@@ -386,11 +439,11 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
386
439
|
if (isIcqqFileElement(message)) {
|
|
387
440
|
switch (target.kind) {
|
|
388
441
|
case 'private': {
|
|
389
|
-
const id = await this.pickFriend(target.userId).sendFile(message.file, message.name);
|
|
442
|
+
const id = await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
|
|
390
443
|
return { message_id: id };
|
|
391
444
|
}
|
|
392
445
|
case 'group': {
|
|
393
|
-
const stat = await this.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
|
|
446
|
+
const stat = await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
|
|
394
447
|
return { message_id: stat.fid };
|
|
395
448
|
}
|
|
396
449
|
case 'temp':
|
|
@@ -400,17 +453,47 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
400
453
|
}
|
|
401
454
|
switch (target.kind) {
|
|
402
455
|
case 'private':
|
|
403
|
-
return this.sendPrivateMsg(target.userId, message);
|
|
456
|
+
return this.client.sendPrivateMsg(target.userId, message);
|
|
404
457
|
case 'group':
|
|
405
|
-
return this.sendGroupMsg(target.groupId, message);
|
|
458
|
+
return this.client.sendGroupMsg(target.groupId, message);
|
|
406
459
|
case 'temp':
|
|
407
|
-
return this.sendTempMsg(target.groupId, target.userId, message);
|
|
460
|
+
return this.client.sendTempMsg(target.groupId, target.userId, message);
|
|
408
461
|
case 'channel':
|
|
409
|
-
return this.sendGuildMsg(target.guildId, target.channelId, message) as unknown as { message_id?: unknown };
|
|
462
|
+
return this.client.sendGuildMsg(target.guildId, target.channelId, message) as unknown as { message_id?: unknown };
|
|
410
463
|
}
|
|
411
464
|
}
|
|
412
465
|
|
|
413
|
-
admit(msg: IcqqInboundMessage
|
|
466
|
+
admit(msg: IcqqInboundMessage): Promise<void> {
|
|
467
|
+
return this.#trackInbound(() => this.#deliverInbound(msg));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#trackInbound(run: () => Promise<void>): Promise<void> {
|
|
471
|
+
const owner = Symbol('icqq-inbound');
|
|
472
|
+
let settle!: () => void;
|
|
473
|
+
const settlement = new Promise<void>((resolve) => { settle = resolve; });
|
|
474
|
+
this.#inflightInbound.add(settlement);
|
|
475
|
+
let operation: Promise<void>;
|
|
476
|
+
try {
|
|
477
|
+
operation = this.#inboundOwner.run(owner, run);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
this.#inflightInbound.delete(settlement);
|
|
480
|
+
settle();
|
|
481
|
+
return Promise.reject(error);
|
|
482
|
+
}
|
|
483
|
+
return operation.finally(() => {
|
|
484
|
+
this.#inflightInbound.delete(settlement);
|
|
485
|
+
settle();
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#assertExternalLifecycle(operation: 'close' | 'stop'): void {
|
|
490
|
+
if (!this.#inboundOwner.getStore()) return;
|
|
491
|
+
throw new Error(
|
|
492
|
+
`ICQQ endpoint ${operation} cannot run inside its own inbound receive; invoke it from the owning lifecycle`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async #deliverInbound(msg: IcqqInboundMessage, admittedBeforeRetire = false): Promise<void> {
|
|
414
497
|
const conversation = msg.conversation;
|
|
415
498
|
if (!this.#open && !admittedBeforeRetire) {
|
|
416
499
|
if (this.#retiring) return;
|
|
@@ -445,7 +528,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
445
528
|
+ (mentioned ? ' (mentioned)' : '')
|
|
446
529
|
+ ` | ${truncatePreview(msg.content, 80)}`,
|
|
447
530
|
);
|
|
448
|
-
|
|
531
|
+
await this.emit('message.receive', {
|
|
449
532
|
conversation,
|
|
450
533
|
message: { conversation, id: msg.id },
|
|
451
534
|
content: msg.content,
|
|
@@ -470,6 +553,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
470
553
|
}
|
|
471
554
|
|
|
472
555
|
#bindClientEvents(): void {
|
|
556
|
+
this.#bindNativeEvents();
|
|
473
557
|
const safe = (label: string, fn: () => void): void => {
|
|
474
558
|
try {
|
|
475
559
|
fn();
|
|
@@ -482,32 +566,104 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
482
566
|
}));
|
|
483
567
|
}
|
|
484
568
|
};
|
|
485
|
-
this.on('message.private', (event) =>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
this.on('
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
this.on('
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
569
|
+
this.client.on('message.private', (event) => {
|
|
570
|
+
this.#startMessageEvent('message.private', event);
|
|
571
|
+
});
|
|
572
|
+
this.client.on('message.group', (event) => {
|
|
573
|
+
this.#startMessageEvent('message.group', event);
|
|
574
|
+
});
|
|
575
|
+
this.client.on('message.guild', (event) => {
|
|
576
|
+
void this.#handleGuildEvent(event).catch((error) => {
|
|
577
|
+
this.#logInboundHandlerFailure('message.guild', error);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
this.client.on('request.friend', (event) => safe('request.friend', () => {
|
|
581
|
+
this.#onRequestEvent(serializeIcqqEvent(event));
|
|
582
|
+
}));
|
|
583
|
+
this.client.on('request.group', (event) => safe('request.group', () => {
|
|
584
|
+
this.#onRequestEvent(serializeIcqqEvent(event));
|
|
585
|
+
}));
|
|
586
|
+
this.client.on('notice.friend', (event) => safe('notice.friend', () => {
|
|
587
|
+
this.#onNoticeEvent(serializeIcqqEvent(event));
|
|
496
588
|
}));
|
|
497
|
-
this.on('
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
this
|
|
589
|
+
this.client.on('notice.group', (event) => safe('notice.group', () => {
|
|
590
|
+
this.#onNoticeEvent(serializeIcqqEvent(event));
|
|
591
|
+
}));
|
|
592
|
+
this.#bindSystemOnlineEvent();
|
|
593
|
+
this.client.on('system.offline', (event) => {
|
|
594
|
+
this.#logSystemEvent('offline', event);
|
|
595
|
+
});
|
|
596
|
+
this.client.on('system.offline.network', (event) => {
|
|
597
|
+
this.#logSystemEvent('offline.network', event);
|
|
598
|
+
});
|
|
599
|
+
this.client.on('system.offline.kickoff', (event) => {
|
|
600
|
+
this.#logSystemEvent('offline.kickoff', event);
|
|
601
|
+
});
|
|
602
|
+
this.client.on('system.login.qrcode', (event) => {
|
|
501
603
|
this.#logger.info('icqq 收到二维码登录事件,等待 LoginAssist / 终端确认后继续');
|
|
502
604
|
this.#handleLoginChallenge('qrcode', event);
|
|
503
605
|
});
|
|
504
|
-
this.on('system.login.device', (event) =>
|
|
505
|
-
|
|
506
|
-
|
|
606
|
+
this.client.on('system.login.device', (event) => {
|
|
607
|
+
this.#handleLoginChallenge('device', event);
|
|
608
|
+
});
|
|
609
|
+
this.client.on('system.login.slider', (event) => {
|
|
610
|
+
this.#handleLoginChallenge('slider', event);
|
|
611
|
+
});
|
|
612
|
+
this.client.on('system.login.error', (event) => {
|
|
507
613
|
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'login_error');
|
|
508
614
|
this.#logSystemEvent('login.error', event);
|
|
509
615
|
});
|
|
510
|
-
this.on('system.login.auth', (event) =>
|
|
616
|
+
this.client.on('system.login.auth', (event) => {
|
|
617
|
+
this.#handleLoginChallenge('auth', event);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#bindSystemOnlineEvent(): void {
|
|
622
|
+
this.client.on('system.online', (event) => {
|
|
623
|
+
if (this.#retiring) return;
|
|
624
|
+
try {
|
|
625
|
+
this.#resolveStartOnline?.();
|
|
626
|
+
this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
|
|
627
|
+
this.#logSystemEvent('online', event);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
this.#logger.warn(formatCompact({
|
|
630
|
+
op: 'icqq_event_handler',
|
|
631
|
+
endpoint: this.endpointName,
|
|
632
|
+
event: 'system.online',
|
|
633
|
+
error: error instanceof Error ? error.message : String(error),
|
|
634
|
+
}));
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Forward every public SDK event, including protocol extensions unknown to Zhin. */
|
|
640
|
+
#bindNativeEvents(): void {
|
|
641
|
+
this.#disposeNativeEvents?.();
|
|
642
|
+
let matchedName: string | undefined;
|
|
643
|
+
const registration = this.client.on(
|
|
644
|
+
(eventName: unknown) => {
|
|
645
|
+
matchedName = typeof eventName === 'string' && !eventName.startsWith('internal.')
|
|
646
|
+
? eventName
|
|
647
|
+
: undefined;
|
|
648
|
+
return matchedName !== undefined;
|
|
649
|
+
},
|
|
650
|
+
(event: unknown) => {
|
|
651
|
+
const name = matchedName;
|
|
652
|
+
if (name) this.#emitNativeEvent(name, event);
|
|
653
|
+
},
|
|
654
|
+
);
|
|
655
|
+
this.#disposeNativeEvents = () => registration();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#emitNativeEvent(name: string, event: unknown): void {
|
|
659
|
+
void this.emitPlatform(name, event).catch((error) => {
|
|
660
|
+
this.#logger.warn(formatCompact({
|
|
661
|
+
op: 'icqq_platform_event_failed',
|
|
662
|
+
endpoint: this.endpointName,
|
|
663
|
+
event: name,
|
|
664
|
+
error: error instanceof Error ? error.message : String(error),
|
|
665
|
+
}));
|
|
666
|
+
});
|
|
511
667
|
}
|
|
512
668
|
|
|
513
669
|
#handleLoginChallenge(
|
|
@@ -520,7 +676,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
520
676
|
this.#logSystemEvent(`login.${step}`, event);
|
|
521
677
|
void runIcqqLoginAssistStep({
|
|
522
678
|
assist: this.#options.loginAssist,
|
|
523
|
-
client: this,
|
|
679
|
+
client: this.client,
|
|
524
680
|
adapter: 'icqq',
|
|
525
681
|
endpointKey: this.endpointName,
|
|
526
682
|
owner: this.#loginAssistOwner,
|
|
@@ -541,11 +697,10 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
541
697
|
}
|
|
542
698
|
|
|
543
699
|
#dispatchIcqqSideEvent(payload: Record<string, unknown>): void {
|
|
544
|
-
const sideEvents = this.#options.sideEvents;
|
|
545
700
|
const postType = String(payload.post_type ?? '');
|
|
546
701
|
const isRequest = postType === 'request' || postType.startsWith('request.');
|
|
547
702
|
const flag = payload.flag != null ? String(payload.flag) : '';
|
|
548
|
-
void receiveOneBotLikeSideEvent(
|
|
703
|
+
void receiveOneBotLikeSideEvent((name, payload) => this.emit(name, payload), {
|
|
549
704
|
adapter: 'icqq',
|
|
550
705
|
endpointKey: this.endpointName,
|
|
551
706
|
platform: 'icqq',
|
|
@@ -565,9 +720,8 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
565
720
|
}
|
|
566
721
|
|
|
567
722
|
#dispatchIcqqSystemSideEvent(type: string, payload: Record<string, unknown> | null): void {
|
|
568
|
-
const sideEvents = this.#options.sideEvents;
|
|
569
723
|
const { scene_type, sub_type } = parseIcqqSystemScene(type);
|
|
570
|
-
void
|
|
724
|
+
void this.emit('system.receive', SystemEvent.from(payload ?? {}, {
|
|
571
725
|
$id: `system:${this.endpointName}:${type}:${Date.now()}`,
|
|
572
726
|
$adapter: 'icqq' as never,
|
|
573
727
|
$endpoint: this.endpointName,
|
|
@@ -660,7 +814,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
660
814
|
timestamp: quoted.time ? quoted.time * (quoted.time < 1e12 ? 1000 : 1) : Date.now(),
|
|
661
815
|
}));
|
|
662
816
|
}
|
|
663
|
-
this
|
|
817
|
+
await this.#deliverInbound({
|
|
664
818
|
id: normalized.messageId,
|
|
665
819
|
conversation,
|
|
666
820
|
content: formatInboundContent(normalized.rawMessage),
|
|
@@ -680,12 +834,9 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
680
834
|
}
|
|
681
835
|
|
|
682
836
|
#startMessageEvent(eventName: 'message.private' | 'message.group', event: unknown): void {
|
|
683
|
-
const operation = this.#handleMessageEvent(eventName, event);
|
|
684
|
-
this.#inflightInbound.add(operation);
|
|
837
|
+
const operation = this.#trackInbound(() => this.#handleMessageEvent(eventName, event));
|
|
685
838
|
void operation.catch((error) => {
|
|
686
839
|
this.#logInboundHandlerFailure(eventName, error);
|
|
687
|
-
}).finally(() => {
|
|
688
|
-
this.#inflightInbound.delete(operation);
|
|
689
840
|
});
|
|
690
841
|
}
|
|
691
842
|
|
|
@@ -694,13 +845,13 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
694
845
|
if (!Number.isFinite(userId) || userId <= 0) return null;
|
|
695
846
|
if (data.message_type === 'group' || data.group_id != null) {
|
|
696
847
|
const groupId = Number(data.group_id);
|
|
697
|
-
return Number.isFinite(groupId) && groupId > 0 ? this.pickGroup(groupId) : null;
|
|
848
|
+
return Number.isFinite(groupId) && groupId > 0 ? this.client.pickGroup(groupId) : null;
|
|
698
849
|
}
|
|
699
850
|
const sourceGroupId = Number(data.sender?.group_id);
|
|
700
851
|
if (Number.isFinite(sourceGroupId) && sourceGroupId > 0) {
|
|
701
|
-
return this.pickMember(sourceGroupId, userId);
|
|
852
|
+
return this.client.pickMember(sourceGroupId, userId);
|
|
702
853
|
}
|
|
703
|
-
return this.pickFriend(userId);
|
|
854
|
+
return this.client.pickFriend(userId);
|
|
704
855
|
}
|
|
705
856
|
|
|
706
857
|
#logInboundHandlerFailure(event: string, error: unknown): void {
|
|
@@ -712,7 +863,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
712
863
|
}));
|
|
713
864
|
}
|
|
714
865
|
|
|
715
|
-
#handleGuildEvent(event: unknown): void {
|
|
866
|
+
async #handleGuildEvent(event: unknown): Promise<void> {
|
|
716
867
|
const payload = serializeIcqqEvent(event);
|
|
717
868
|
if (!payload || typeof payload !== 'object') return;
|
|
718
869
|
const normalized = normalizeIcqqGuildInboundMessage(
|
|
@@ -736,7 +887,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
736
887
|
}));
|
|
737
888
|
return;
|
|
738
889
|
}
|
|
739
|
-
this.admit({
|
|
890
|
+
await this.admit({
|
|
740
891
|
id: normalized.messageId,
|
|
741
892
|
conversation: icqqInboundConversation(String(this.#options.id), {
|
|
742
893
|
channelType: 'channel',
|
|
@@ -774,7 +925,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
774
925
|
}
|
|
775
926
|
|
|
776
927
|
async #listPendingRequests(): Promise<readonly EndpointPendingRequest[]> {
|
|
777
|
-
const messages = await this.getSystemMsg();
|
|
928
|
+
const messages = await this.client.getSystemMsg();
|
|
778
929
|
const rows: EndpointPendingRequest[] = [];
|
|
779
930
|
for (const raw of messages) {
|
|
780
931
|
const msg = raw as unknown as IcqqSystemMessage & { request_type?: string };
|
|
@@ -802,7 +953,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
802
953
|
|
|
803
954
|
async #pullPendingSystemMessages(signal: AbortSignal): Promise<void> {
|
|
804
955
|
try {
|
|
805
|
-
const messages = await raceAbort(Promise.resolve(this.getSystemMsg()), signal);
|
|
956
|
+
const messages = await raceAbort(Promise.resolve(this.client.getSystemMsg()), signal);
|
|
806
957
|
for (const raw of messages) {
|
|
807
958
|
signal.throwIfAborted();
|
|
808
959
|
const msg = raw as unknown as IcqqSystemMessage & { request_type?: string };
|
|
@@ -831,7 +982,7 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
831
982
|
}
|
|
832
983
|
|
|
833
984
|
async #approveRequest(id: string, remark?: string): Promise<void> {
|
|
834
|
-
const messages = await this.getSystemMsg();
|
|
985
|
+
const messages = await this.client.getSystemMsg();
|
|
835
986
|
const target = messages.find((m) => {
|
|
836
987
|
const msg = m as unknown as IcqqSystemMessage;
|
|
837
988
|
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
@@ -840,14 +991,14 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
840
991
|
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
841
992
|
}
|
|
842
993
|
if ((target as { request_type?: string }).request_type === 'friend') {
|
|
843
|
-
await this.setFriendAddRequest(target.flag, true, remark);
|
|
994
|
+
await this.client.setFriendAddRequest(target.flag, true, remark);
|
|
844
995
|
} else {
|
|
845
|
-
await this.setGroupAddRequest(target.flag, true);
|
|
996
|
+
await this.client.setGroupAddRequest(target.flag, true);
|
|
846
997
|
}
|
|
847
998
|
}
|
|
848
999
|
|
|
849
1000
|
async #rejectRequest(id: string, reason?: string): Promise<void> {
|
|
850
|
-
const messages = await this.getSystemMsg();
|
|
1001
|
+
const messages = await this.client.getSystemMsg();
|
|
851
1002
|
const target = messages.find((m) => {
|
|
852
1003
|
const msg = m as unknown as IcqqSystemMessage;
|
|
853
1004
|
return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
|
|
@@ -856,9 +1007,9 @@ export class IcqqEndpoint extends Client implements EndpointInstance {
|
|
|
856
1007
|
throw new Error(`icqq 未找到待处理请求: ${id}`);
|
|
857
1008
|
}
|
|
858
1009
|
if ((target as { request_type?: string }).request_type === 'friend') {
|
|
859
|
-
await this.setFriendAddRequest(target.flag, false);
|
|
1010
|
+
await this.client.setFriendAddRequest(target.flag, false);
|
|
860
1011
|
} else {
|
|
861
|
-
await this.setGroupAddRequest(target.flag, false, reason);
|
|
1012
|
+
await this.client.setGroupAddRequest(target.flag, false, reason);
|
|
862
1013
|
}
|
|
863
1014
|
}
|
|
864
1015
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,12 +20,10 @@ export {
|
|
|
20
20
|
export * from './types.js';
|
|
21
21
|
|
|
22
22
|
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
type IcqqAgentEndpoint,
|
|
28
|
-
} from './icqq-agent-deps.js';
|
|
23
|
+
icqqClient,
|
|
24
|
+
type IcqqClient,
|
|
25
|
+
type IcqqClientEventMap,
|
|
26
|
+
} from './client.js';
|
|
29
27
|
|
|
30
28
|
export {
|
|
31
29
|
resolveIcqqInboundMessageId,
|
package/tools/announce.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { getIcqqAgentDeps } from "../lib/icqq-agent-deps.js";
|
|
5
4
|
export default defineAgentTool({
|
|
6
5
|
description: '发送 QQ 群公告(需要管理员权限)',
|
|
7
6
|
inputSchema: z.object({
|
|
8
|
-
endpoint_id: z.string().describe('Endpoint QQ号'),
|
|
9
7
|
group_id: z.number().describe('目标群号'),
|
|
10
8
|
content: z.string().describe('公告内容'),
|
|
11
9
|
}),
|
|
12
|
-
|
|
10
|
+
adapter: 'icqq',
|
|
13
11
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
14
|
-
async execute({
|
|
15
|
-
|
|
16
|
-
await endpoint.sendGroupNotice(group_id, content);
|
|
12
|
+
async execute({ group_id, content }, context) {
|
|
13
|
+
await context.$client.sendGroupNotice(group_id, content);
|
|
17
14
|
return { success: true, message: '群公告已发送' };
|
|
18
15
|
},
|
|
19
16
|
});
|