@zhin.js/adapter-icqq 8.0.14 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +82 -3
  3. package/adapters/icqq.js +1 -3
  4. package/adapters/icqq.ts +2 -8
  5. package/agent/prompt-sections/platform.ts +17 -0
  6. package/lib/client.d.ts +19 -0
  7. package/lib/client.js +3 -0
  8. package/lib/endpoint.d.ts +9 -6
  9. package/lib/endpoint.js +232 -83
  10. package/lib/icqq-endpoint-commands.d.ts +1 -1
  11. package/lib/index.d.ts +1 -1
  12. package/lib/index.js +1 -1
  13. package/package.json +21 -14
  14. package/plugin.js +1 -24
  15. package/src/client.ts +29 -0
  16. package/src/endpoint.ts +236 -85
  17. package/src/index.ts +4 -6
  18. package/tools/announce.js +3 -6
  19. package/tools/announce.ts +4 -7
  20. package/tools/essence.js +5 -7
  21. package/tools/essence.ts +6 -8
  22. package/tools/friend_list.js +6 -9
  23. package/tools/friend_list.ts +7 -10
  24. package/tools/group_files.js +3 -6
  25. package/tools/group_files.ts +4 -7
  26. package/tools/group_invite.js +6 -8
  27. package/tools/group_invite.ts +7 -9
  28. package/tools/group_kick.js +6 -8
  29. package/tools/group_kick.ts +7 -9
  30. package/tools/group_list.js +7 -10
  31. package/tools/group_list.ts +8 -11
  32. package/tools/group_member_info.js +3 -6
  33. package/tools/group_member_info.ts +3 -7
  34. package/tools/group_sign.js +5 -7
  35. package/tools/group_sign.ts +6 -8
  36. package/tools/list_muted.js +3 -6
  37. package/tools/list_muted.ts +4 -7
  38. package/tools/poke.js +3 -6
  39. package/tools/poke.ts +4 -7
  40. package/tools/send_user_like.js +3 -6
  41. package/tools/send_user_like.ts +4 -7
  42. package/tools/set_anonymous.js +5 -7
  43. package/tools/set_anonymous.ts +6 -8
  44. package/tools/set_mute.js +6 -8
  45. package/tools/set_mute.ts +7 -9
  46. package/tools/set_title.js +6 -8
  47. package/tools/set_title.ts +7 -9
  48. package/tools/user_info.js +3 -6
  49. package/tools/user_info.ts +4 -7
  50. package/lib/agent-prompt.d.ts +0 -2
  51. package/lib/agent-prompt.js +0 -97
  52. package/lib/icqq-agent-deps.d.ts +0 -13
  53. package/lib/icqq-agent-deps.js +0 -27
  54. package/src/agent-prompt.ts +0 -110
  55. 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
- export class IcqqEndpoint extends Client implements EndpointInstance {
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
- super(Number(options.config.id), nativeConfig);
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) this.admit(msg);
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.#retiring = true;
323
- this.#open = false;
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.#retiring = true;
329
- this.#open = false;
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.#unregisterAgent?.();
335
- this.#unregisterAgent = undefined;
336
- for (const event of BOUND_EVENTS) this.off(event);
337
- try {
338
- await this.logout?.();
339
- } catch { /* ignore */ }
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, admittedBeforeRetire = false): void {
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
- void this.#options.gateway.receive({
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) => this.#startMessageEvent('message.private', event));
486
- this.on('message.group', (event) => this.#startMessageEvent('message.group', event));
487
- this.on('message.guild', (event) => safe('message.guild', () => this.#handleGuildEvent(event)));
488
- this.on('request.friend', (event) => safe('request.friend', () => this.#onRequestEvent(serializeIcqqEvent(event))));
489
- this.on('request.group', (event) => safe('request.group', () => this.#onRequestEvent(serializeIcqqEvent(event))));
490
- this.on('notice.friend', (event) => safe('notice.friend', () => this.#onNoticeEvent(serializeIcqqEvent(event))));
491
- this.on('notice.group', (event) => safe('notice.group', () => this.#onNoticeEvent(serializeIcqqEvent(event))));
492
- this.on('system.online', (event) => safe('system.online', () => {
493
- this.#resolveStartOnline?.();
494
- this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
495
- this.#logSystemEvent('online', event);
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('system.offline', (event) => this.#logSystemEvent('offline', event));
498
- this.on('system.offline.network', (event) => this.#logSystemEvent('offline.network', event));
499
- this.on('system.offline.kickoff', (event) => this.#logSystemEvent('offline.kickoff', event));
500
- this.on('system.login.qrcode', (event) => {
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) => this.#handleLoginChallenge('device', event));
505
- this.on('system.login.slider', (event) => this.#handleLoginChallenge('slider', event));
506
- this.on('system.login.error', (event) => {
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) => this.#handleLoginChallenge('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(sideEvents, {
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 sideEvents.receiveSystem(SystemEvent.from(payload ?? {}, {
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.admit({
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
- getIcqqAgentDeps,
24
- registerIcqqAgentEndpoint,
25
- setIcqqAgentDeps,
26
- type IcqqAgentDeps,
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
- platforms: ['icqq'],
10
+ adapter: 'icqq',
13
11
  permissions: ['role(master,admin,trusted,owner)'],
14
- async execute({ endpoint_id, group_id, content }) {
15
- const endpoint = getIcqqAgentDeps().getEndpoint(endpoint_id);
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
  });