@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +28 -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/lib/endpoint.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Client, parseGroupMessageId, } from '@icqqjs/icqq';
2
2
  import { inspect } from 'node:util';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { Endpoint } from 'zhin.js/adapter';
3
5
  import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments } from '@zhin.js/core';
4
6
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
5
- import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
6
7
  import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
7
8
  import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, messageLookupFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveIcqqInboundMedia, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
8
9
  import { isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
@@ -28,7 +29,16 @@ const BOUND_EVENTS = [
28
29
  'system.login.error',
29
30
  'system.login.auth',
30
31
  ];
31
- export class IcqqEndpoint extends Client {
32
+ class ManagedIcqqClient extends Client {
33
+ cancelReconnect() {
34
+ if (!this.login_timer)
35
+ return;
36
+ clearTimeout(this.login_timer);
37
+ this.login_timer = null;
38
+ }
39
+ }
40
+ export class IcqqEndpoint extends Endpoint {
41
+ client;
32
42
  #logger;
33
43
  #options;
34
44
  endpointName;
@@ -39,10 +49,13 @@ export class IcqqEndpoint extends Client {
39
49
  #open = false;
40
50
  #retiring = false;
41
51
  #started = false;
52
+ #retirementEpoch = 0;
53
+ #retirementArmed = false;
54
+ #disposeNativeEvents;
42
55
  #resolveStartOnline;
43
56
  #heldInbound = [];
44
- #unregisterAgent;
45
57
  #inflightInbound = new Set();
58
+ #inboundOwner = new AsyncLocalStorage();
46
59
  #messageContent = new Map();
47
60
  content = Object.freeze({
48
61
  resolve: async (reference, context) => {
@@ -85,7 +98,7 @@ export class IcqqEndpoint extends Client {
85
98
  // ICQQ does not expose a cancellable getForwardMsg API. Keep the caller's
86
99
  // generation lease until the native operation actually settles, then honor
87
100
  // cancellation before publishing any resolved content.
88
- const raw = await this.getForwardMsg(forwardId);
101
+ const raw = await this.client.getForwardMsg(forwardId);
89
102
  context.signal.throwIfAborted();
90
103
  const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
91
104
  state.remainingEntries -= entries.length;
@@ -118,41 +131,41 @@ export class IcqqEndpoint extends Client {
118
131
  }
119
132
  }
120
133
  management = Object.freeze({
121
- listFriends: async () => Array.from(this.fl.values()).map((f) => ({
134
+ listFriends: async () => Array.from(this.client.fl.values()).map((f) => ({
122
135
  user_id: f.user_id,
123
136
  nickname: f.nickname,
124
137
  remark: f.remark ?? '',
125
138
  })),
126
- listGroups: async () => Array.from(this.gl.values()).map((g) => ({
139
+ listGroups: async () => Array.from(this.client.gl.values()).map((g) => ({
127
140
  group_id: g.group_id,
128
141
  name: g.group_name,
129
142
  })),
130
143
  listChannels: async () => {
131
- await this.#guildCatalog.syncAll(this);
144
+ await this.#guildCatalog.syncAll(this.client);
132
145
  return this.#guildCatalog.getGuildChannelList();
133
146
  },
134
- listGroupMembers: async (groupId) => Array.from((await this.getGroupMemberList(Number(groupId))).values()),
147
+ listGroupMembers: async (groupId) => Array.from((await this.client.getGroupMemberList(Number(groupId))).values()),
135
148
  listRequests: () => this.#listPendingRequests(),
136
149
  approveRequest: (requestId, remark) => this.#approveRequest(requestId, remark),
137
150
  rejectRequest: (requestId, reason) => this.#rejectRequest(requestId, reason),
138
151
  kickGroupMember: async (groupId, userId) => {
139
- await this.setGroupKick(Number(groupId), Number(userId));
152
+ await this.client.setGroupKick(Number(groupId), Number(userId));
140
153
  },
141
154
  muteGroupMember: async (groupId, userId, duration) => {
142
- await this.setGroupBan(Number(groupId), Number(userId), duration);
155
+ await this.client.setGroupBan(Number(groupId), Number(userId), duration);
143
156
  },
144
157
  setGroupAdmin: async (groupId, userId, enabled) => {
145
- await this.setGroupAdmin(Number(groupId), Number(userId), enabled);
158
+ await this.client.setGroupAdmin(Number(groupId), Number(userId), enabled);
146
159
  },
147
160
  deleteFriend: async (userId) => {
148
- await this.deleteFriend(Number(userId));
161
+ await this.client.deleteFriend(Number(userId));
149
162
  },
150
163
  });
151
164
  control = Object.freeze({
152
165
  recall: async (message) => {
153
166
  if (!message.id || message.id.startsWith('outbound:'))
154
167
  return;
155
- await this.deleteMsg(message.id);
168
+ await this.client.deleteMsg(message.id);
156
169
  },
157
170
  addReaction: async (message, emoji) => {
158
171
  const target = resolveIcqqGroupReactionTarget(message);
@@ -160,7 +173,7 @@ export class IcqqEndpoint extends Client {
160
173
  return null;
161
174
  try {
162
175
  // Do not await protocol ACK — packet timeout must not stall the AI reply.
163
- this.#ignoreReactionAck('add', this.pickGroup(target.groupId).setReaction(target.seq, emoji));
176
+ this.#ignoreReactionAck('add', this.client.pickGroup(target.groupId).setReaction(target.seq, emoji));
164
177
  }
165
178
  catch (error) {
166
179
  this.#logger.debug(formatCompact({
@@ -175,7 +188,7 @@ export class IcqqEndpoint extends Client {
175
188
  if (!target)
176
189
  return;
177
190
  try {
178
- this.#ignoreReactionAck('remove', this.pickGroup(target.groupId).delReaction(target.seq, reactionId));
191
+ this.#ignoreReactionAck('remove', this.client.pickGroup(target.groupId).delReaction(target.seq, reactionId));
179
192
  }
180
193
  catch (error) {
181
194
  this.#logger.debug(formatCompact({
@@ -186,8 +199,9 @@ export class IcqqEndpoint extends Client {
186
199
  },
187
200
  });
188
201
  constructor(options) {
202
+ super();
189
203
  const nativeConfig = resolveNativeClientConfig(options.config);
190
- super(Number(options.config.id), nativeConfig);
204
+ this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig);
191
205
  this.#logger = getAdapterLogger('icqq', options.config.id);
192
206
  this.#options = options;
193
207
  this.endpointName = options.config.id;
@@ -204,6 +218,12 @@ export class IcqqEndpoint extends Client {
204
218
  if (this.#started)
205
219
  return;
206
220
  signal.throwIfAborted();
221
+ // Invalidate a guard left by a prior lifecycle epoch before admitting a
222
+ // new login attempt on this endpoint instance.
223
+ this.#retirementEpoch += 1;
224
+ this.#retirementArmed = false;
225
+ this.#retiring = false;
226
+ this.client.off('system.online');
207
227
  this.#started = true;
208
228
  let resolveOnline;
209
229
  const onlineReady = new Promise((resolve) => {
@@ -214,13 +234,12 @@ export class IcqqEndpoint extends Client {
214
234
  signal.addEventListener('abort', onAbort, { once: true });
215
235
  try {
216
236
  this.#bindClientEvents();
217
- await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
237
+ await raceAbort(Promise.resolve(this.client.login(this.#options.config.password)), signal);
218
238
  await onlineReady;
219
239
  signal.throwIfAborted();
220
240
  await this.#pullPendingSystemMessages(signal);
221
241
  signal.throwIfAborted();
222
- this.#unregisterAgent = registerIcqqAgentEndpoint(this.endpointName, this);
223
- this.#logger.info(`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`);
242
+ this.#logger.info(`connected (direct) | friends: ${this.client.fl.size} | groups: ${this.client.gl.size}`);
224
243
  }
225
244
  catch (error) {
226
245
  await this.stop();
@@ -234,34 +253,48 @@ export class IcqqEndpoint extends Client {
234
253
  }
235
254
  }
236
255
  open() {
256
+ if (this.#retirementArmed) {
257
+ this.#retirementEpoch += 1;
258
+ this.#retirementArmed = false;
259
+ this.client.off('system.online');
260
+ this.#bindSystemOnlineEvent();
261
+ }
237
262
  this.#retiring = false;
238
263
  this.#open = true;
239
264
  const held = this.#heldInbound.splice(0);
240
- for (const msg of held)
241
- this.admit(msg);
265
+ for (const msg of held) {
266
+ void this.admit(msg).catch((error) => {
267
+ this.#logInboundHandlerFailure('held', error);
268
+ });
269
+ }
242
270
  }
243
271
  async close() {
244
- this.#retiring = true;
245
- this.#open = false;
272
+ this.#assertExternalLifecycle('close');
273
+ this.#armRetirementFence();
246
274
  await Promise.allSettled([...this.#inflightInbound]);
247
275
  }
248
276
  async stop() {
249
- this.#retiring = true;
250
- this.#open = false;
277
+ this.#assertExternalLifecycle('stop');
278
+ this.#armRetirementFence();
251
279
  await Promise.allSettled([...this.#inflightInbound]);
252
280
  this.#resolveStartOnline?.();
253
281
  this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'endpoint_stopped');
254
282
  this.#heldInbound.length = 0;
255
- this.#unregisterAgent?.();
256
- this.#unregisterAgent = undefined;
257
- for (const event of BOUND_EVENTS)
258
- this.off(event);
259
- try {
260
- await this.logout?.();
283
+ this.#disposeNativeEvents?.();
284
+ this.#disposeNativeEvents = undefined;
285
+ for (const event of BOUND_EVENTS) {
286
+ // The retirement guard owns this matcher until a late login handshake
287
+ // has finished. start()/open() replace it with the normal listener.
288
+ if (event !== 'system.online')
289
+ this.client.off(event);
261
290
  }
262
- catch { /* ignore */ }
291
+ // `Client.logout()` sends an account-level unregister packet before it
292
+ // closes this transport. During config HMR the replacement endpoint for
293
+ // the same UIN is already online, so unregistering the retiring generation
294
+ // also closes the replacement session. Endpoint ownership is transport
295
+ // scoped: retire only this Client's TCP connection.
263
296
  try {
264
- this.terminate();
297
+ this.client.terminate();
265
298
  }
266
299
  catch { /* ignore */ }
267
300
  this.#inboundDeduper.clear();
@@ -270,6 +303,30 @@ export class IcqqEndpoint extends Client {
270
303
  this.#started = false;
271
304
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
272
305
  }
306
+ #armRetirementFence() {
307
+ this.#retiring = true;
308
+ this.#open = false;
309
+ if (this.#retirementArmed)
310
+ return;
311
+ this.#retirementArmed = true;
312
+ const retirementEpoch = ++this.#retirementEpoch;
313
+ // ICQQ owns reconnect through this per-Client timer. Clear it before any
314
+ // asynchronous drain so a queued callback cannot enter while close waits.
315
+ this.client.cancelReconnect();
316
+ // A login callback may already be inside the SDK handshake, which has no
317
+ // abort API. Replace the normal online listener with an epoch-owned guard
318
+ // before draining inbound work. Transport termination is account-local;
319
+ // logout would also unregister an HMR replacement using the same UIN.
320
+ this.client.off('system.online');
321
+ this.client.on('system.online', () => {
322
+ if (this.#retirementEpoch !== retirementEpoch || !this.#retiring)
323
+ return;
324
+ try {
325
+ this.client.terminate();
326
+ }
327
+ catch { /* already closed */ }
328
+ });
329
+ }
273
330
  async send({ conversation, payload }) {
274
331
  if (!this.#started)
275
332
  throw new Error('icqq endpoint 未连接');
@@ -306,11 +363,11 @@ export class IcqqEndpoint extends Client {
306
363
  if (isIcqqFileElement(message)) {
307
364
  switch (target.kind) {
308
365
  case 'private': {
309
- const id = await this.pickFriend(target.userId).sendFile(message.file, message.name);
366
+ const id = await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
310
367
  return { message_id: id };
311
368
  }
312
369
  case 'group': {
313
- const stat = await this.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
370
+ const stat = await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
314
371
  return { message_id: stat.fid };
315
372
  }
316
373
  case 'temp':
@@ -320,16 +377,43 @@ export class IcqqEndpoint extends Client {
320
377
  }
321
378
  switch (target.kind) {
322
379
  case 'private':
323
- return this.sendPrivateMsg(target.userId, message);
380
+ return this.client.sendPrivateMsg(target.userId, message);
324
381
  case 'group':
325
- return this.sendGroupMsg(target.groupId, message);
382
+ return this.client.sendGroupMsg(target.groupId, message);
326
383
  case 'temp':
327
- return this.sendTempMsg(target.groupId, target.userId, message);
384
+ return this.client.sendTempMsg(target.groupId, target.userId, message);
328
385
  case 'channel':
329
- return this.sendGuildMsg(target.guildId, target.channelId, message);
386
+ return this.client.sendGuildMsg(target.guildId, target.channelId, message);
387
+ }
388
+ }
389
+ admit(msg) {
390
+ return this.#trackInbound(() => this.#deliverInbound(msg));
391
+ }
392
+ #trackInbound(run) {
393
+ const owner = Symbol('icqq-inbound');
394
+ let settle;
395
+ const settlement = new Promise((resolve) => { settle = resolve; });
396
+ this.#inflightInbound.add(settlement);
397
+ let operation;
398
+ try {
399
+ operation = this.#inboundOwner.run(owner, run);
400
+ }
401
+ catch (error) {
402
+ this.#inflightInbound.delete(settlement);
403
+ settle();
404
+ return Promise.reject(error);
330
405
  }
406
+ return operation.finally(() => {
407
+ this.#inflightInbound.delete(settlement);
408
+ settle();
409
+ });
331
410
  }
332
- admit(msg, admittedBeforeRetire = false) {
411
+ #assertExternalLifecycle(operation) {
412
+ if (!this.#inboundOwner.getStore())
413
+ return;
414
+ throw new Error(`ICQQ endpoint ${operation} cannot run inside its own inbound receive; invoke it from the owning lifecycle`);
415
+ }
416
+ async #deliverInbound(msg, admittedBeforeRetire = false) {
333
417
  const conversation = msg.conversation;
334
418
  if (!this.#open && !admittedBeforeRetire) {
335
419
  if (this.#retiring)
@@ -361,7 +445,7 @@ export class IcqqEndpoint extends Client {
361
445
  + (msg.sender ? ` from ${msg.sender.name ?? msg.sender.id}` : '')
362
446
  + (mentioned ? ' (mentioned)' : '')
363
447
  + ` | ${truncatePreview(msg.content, 80)}`);
364
- void this.#options.gateway.receive({
448
+ await this.emit('message.receive', {
365
449
  conversation,
366
450
  message: { conversation, id: msg.id },
367
451
  content: msg.content,
@@ -385,6 +469,7 @@ export class IcqqEndpoint extends Client {
385
469
  });
386
470
  }
387
471
  #bindClientEvents() {
472
+ this.#bindNativeEvents();
388
473
  const safe = (label, fn) => {
389
474
  try {
390
475
  fn();
@@ -398,32 +483,101 @@ export class IcqqEndpoint extends Client {
398
483
  }));
399
484
  }
400
485
  };
401
- this.on('message.private', (event) => this.#startMessageEvent('message.private', event));
402
- this.on('message.group', (event) => this.#startMessageEvent('message.group', event));
403
- this.on('message.guild', (event) => safe('message.guild', () => this.#handleGuildEvent(event)));
404
- this.on('request.friend', (event) => safe('request.friend', () => this.#onRequestEvent(serializeIcqqEvent(event))));
405
- this.on('request.group', (event) => safe('request.group', () => this.#onRequestEvent(serializeIcqqEvent(event))));
406
- this.on('notice.friend', (event) => safe('notice.friend', () => this.#onNoticeEvent(serializeIcqqEvent(event))));
407
- this.on('notice.group', (event) => safe('notice.group', () => this.#onNoticeEvent(serializeIcqqEvent(event))));
408
- this.on('system.online', (event) => safe('system.online', () => {
409
- this.#resolveStartOnline?.();
410
- this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
411
- this.#logSystemEvent('online', event);
486
+ this.client.on('message.private', (event) => {
487
+ this.#startMessageEvent('message.private', event);
488
+ });
489
+ this.client.on('message.group', (event) => {
490
+ this.#startMessageEvent('message.group', event);
491
+ });
492
+ this.client.on('message.guild', (event) => {
493
+ void this.#handleGuildEvent(event).catch((error) => {
494
+ this.#logInboundHandlerFailure('message.guild', error);
495
+ });
496
+ });
497
+ this.client.on('request.friend', (event) => safe('request.friend', () => {
498
+ this.#onRequestEvent(serializeIcqqEvent(event));
499
+ }));
500
+ this.client.on('request.group', (event) => safe('request.group', () => {
501
+ this.#onRequestEvent(serializeIcqqEvent(event));
502
+ }));
503
+ this.client.on('notice.friend', (event) => safe('notice.friend', () => {
504
+ this.#onNoticeEvent(serializeIcqqEvent(event));
412
505
  }));
413
- this.on('system.offline', (event) => this.#logSystemEvent('offline', event));
414
- this.on('system.offline.network', (event) => this.#logSystemEvent('offline.network', event));
415
- this.on('system.offline.kickoff', (event) => this.#logSystemEvent('offline.kickoff', event));
416
- this.on('system.login.qrcode', (event) => {
506
+ this.client.on('notice.group', (event) => safe('notice.group', () => {
507
+ this.#onNoticeEvent(serializeIcqqEvent(event));
508
+ }));
509
+ this.#bindSystemOnlineEvent();
510
+ this.client.on('system.offline', (event) => {
511
+ this.#logSystemEvent('offline', event);
512
+ });
513
+ this.client.on('system.offline.network', (event) => {
514
+ this.#logSystemEvent('offline.network', event);
515
+ });
516
+ this.client.on('system.offline.kickoff', (event) => {
517
+ this.#logSystemEvent('offline.kickoff', event);
518
+ });
519
+ this.client.on('system.login.qrcode', (event) => {
417
520
  this.#logger.info('icqq 收到二维码登录事件,等待 LoginAssist / 终端确认后继续');
418
521
  this.#handleLoginChallenge('qrcode', event);
419
522
  });
420
- this.on('system.login.device', (event) => this.#handleLoginChallenge('device', event));
421
- this.on('system.login.slider', (event) => this.#handleLoginChallenge('slider', event));
422
- this.on('system.login.error', (event) => {
523
+ this.client.on('system.login.device', (event) => {
524
+ this.#handleLoginChallenge('device', event);
525
+ });
526
+ this.client.on('system.login.slider', (event) => {
527
+ this.#handleLoginChallenge('slider', event);
528
+ });
529
+ this.client.on('system.login.error', (event) => {
423
530
  this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'login_error');
424
531
  this.#logSystemEvent('login.error', event);
425
532
  });
426
- this.on('system.login.auth', (event) => this.#handleLoginChallenge('auth', event));
533
+ this.client.on('system.login.auth', (event) => {
534
+ this.#handleLoginChallenge('auth', event);
535
+ });
536
+ }
537
+ #bindSystemOnlineEvent() {
538
+ this.client.on('system.online', (event) => {
539
+ if (this.#retiring)
540
+ return;
541
+ try {
542
+ this.#resolveStartOnline?.();
543
+ this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
544
+ this.#logSystemEvent('online', event);
545
+ }
546
+ catch (error) {
547
+ this.#logger.warn(formatCompact({
548
+ op: 'icqq_event_handler',
549
+ endpoint: this.endpointName,
550
+ event: 'system.online',
551
+ error: error instanceof Error ? error.message : String(error),
552
+ }));
553
+ }
554
+ });
555
+ }
556
+ /** Forward every public SDK event, including protocol extensions unknown to Zhin. */
557
+ #bindNativeEvents() {
558
+ this.#disposeNativeEvents?.();
559
+ let matchedName;
560
+ const registration = this.client.on((eventName) => {
561
+ matchedName = typeof eventName === 'string' && !eventName.startsWith('internal.')
562
+ ? eventName
563
+ : undefined;
564
+ return matchedName !== undefined;
565
+ }, (event) => {
566
+ const name = matchedName;
567
+ if (name)
568
+ this.#emitNativeEvent(name, event);
569
+ });
570
+ this.#disposeNativeEvents = () => registration();
571
+ }
572
+ #emitNativeEvent(name, event) {
573
+ void this.emitPlatform(name, event).catch((error) => {
574
+ this.#logger.warn(formatCompact({
575
+ op: 'icqq_platform_event_failed',
576
+ endpoint: this.endpointName,
577
+ event: name,
578
+ error: error instanceof Error ? error.message : String(error),
579
+ }));
580
+ });
427
581
  }
428
582
  #handleLoginChallenge(step, event) {
429
583
  const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
@@ -432,7 +586,7 @@ export class IcqqEndpoint extends Client {
432
586
  this.#logSystemEvent(`login.${step}`, event);
433
587
  void runIcqqLoginAssistStep({
434
588
  assist: this.#options.loginAssist,
435
- client: this,
589
+ client: this.client,
436
590
  adapter: 'icqq',
437
591
  endpointKey: this.endpointName,
438
592
  owner: this.#loginAssistOwner,
@@ -451,11 +605,10 @@ export class IcqqEndpoint extends Client {
451
605
  this.#dispatchIcqqSystemSideEvent(type, payload);
452
606
  }
453
607
  #dispatchIcqqSideEvent(payload) {
454
- const sideEvents = this.#options.sideEvents;
455
608
  const postType = String(payload.post_type ?? '');
456
609
  const isRequest = postType === 'request' || postType.startsWith('request.');
457
610
  const flag = payload.flag != null ? String(payload.flag) : '';
458
- void receiveOneBotLikeSideEvent(sideEvents, {
611
+ void receiveOneBotLikeSideEvent((name, payload) => this.emit(name, payload), {
459
612
  adapter: 'icqq',
460
613
  endpointKey: this.endpointName,
461
614
  platform: 'icqq',
@@ -474,9 +627,8 @@ export class IcqqEndpoint extends Client {
474
627
  });
475
628
  }
476
629
  #dispatchIcqqSystemSideEvent(type, payload) {
477
- const sideEvents = this.#options.sideEvents;
478
630
  const { scene_type, sub_type } = parseIcqqSystemScene(type);
479
- void sideEvents.receiveSystem(SystemEvent.from(payload ?? {}, {
631
+ void this.emit('system.receive', SystemEvent.from(payload ?? {}, {
480
632
  $id: `system:${this.endpointName}:${type}:${Date.now()}`,
481
633
  $adapter: 'icqq',
482
634
  $endpoint: this.endpointName,
@@ -568,7 +720,7 @@ export class IcqqEndpoint extends Client {
568
720
  timestamp: quoted.time ? quoted.time * (quoted.time < 1e12 ? 1000 : 1) : Date.now(),
569
721
  }));
570
722
  }
571
- this.admit({
723
+ await this.#deliverInbound({
572
724
  id: normalized.messageId,
573
725
  conversation,
574
726
  content: formatInboundContent(normalized.rawMessage),
@@ -587,12 +739,9 @@ export class IcqqEndpoint extends Client {
587
739
  }, admittedBeforeRetire);
588
740
  }
589
741
  #startMessageEvent(eventName, event) {
590
- const operation = this.#handleMessageEvent(eventName, event);
591
- this.#inflightInbound.add(operation);
742
+ const operation = this.#trackInbound(() => this.#handleMessageEvent(eventName, event));
592
743
  void operation.catch((error) => {
593
744
  this.#logInboundHandlerFailure(eventName, error);
594
- }).finally(() => {
595
- this.#inflightInbound.delete(operation);
596
745
  });
597
746
  }
598
747
  #resolveInboundContact(data) {
@@ -601,13 +750,13 @@ export class IcqqEndpoint extends Client {
601
750
  return null;
602
751
  if (data.message_type === 'group' || data.group_id != null) {
603
752
  const groupId = Number(data.group_id);
604
- return Number.isFinite(groupId) && groupId > 0 ? this.pickGroup(groupId) : null;
753
+ return Number.isFinite(groupId) && groupId > 0 ? this.client.pickGroup(groupId) : null;
605
754
  }
606
755
  const sourceGroupId = Number(data.sender?.group_id);
607
756
  if (Number.isFinite(sourceGroupId) && sourceGroupId > 0) {
608
- return this.pickMember(sourceGroupId, userId);
757
+ return this.client.pickMember(sourceGroupId, userId);
609
758
  }
610
- return this.pickFriend(userId);
759
+ return this.client.pickFriend(userId);
611
760
  }
612
761
  #logInboundHandlerFailure(event, error) {
613
762
  this.#logger.warn(formatCompact({
@@ -617,7 +766,7 @@ export class IcqqEndpoint extends Client {
617
766
  error: describeUnknownError(error),
618
767
  }));
619
768
  }
620
- #handleGuildEvent(event) {
769
+ async #handleGuildEvent(event) {
621
770
  const payload = serializeIcqqEvent(event);
622
771
  if (!payload || typeof payload !== 'object')
623
772
  return;
@@ -640,7 +789,7 @@ export class IcqqEndpoint extends Client {
640
789
  }));
641
790
  return;
642
791
  }
643
- this.admit({
792
+ await this.admit({
644
793
  id: normalized.messageId,
645
794
  conversation: icqqInboundConversation(String(this.#options.id), {
646
795
  channelType: 'channel',
@@ -679,7 +828,7 @@ export class IcqqEndpoint extends Client {
679
828
  this.#dispatchIcqqSideEvent(payload);
680
829
  }
681
830
  async #listPendingRequests() {
682
- const messages = await this.getSystemMsg();
831
+ const messages = await this.client.getSystemMsg();
683
832
  const rows = [];
684
833
  for (const raw of messages) {
685
834
  const msg = raw;
@@ -707,7 +856,7 @@ export class IcqqEndpoint extends Client {
707
856
  }
708
857
  async #pullPendingSystemMessages(signal) {
709
858
  try {
710
- const messages = await raceAbort(Promise.resolve(this.getSystemMsg()), signal);
859
+ const messages = await raceAbort(Promise.resolve(this.client.getSystemMsg()), signal);
711
860
  for (const raw of messages) {
712
861
  signal.throwIfAborted();
713
862
  const msg = raw;
@@ -738,7 +887,7 @@ export class IcqqEndpoint extends Client {
738
887
  }
739
888
  }
740
889
  async #approveRequest(id, remark) {
741
- const messages = await this.getSystemMsg();
890
+ const messages = await this.client.getSystemMsg();
742
891
  const target = messages.find((m) => {
743
892
  const msg = m;
744
893
  return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
@@ -747,14 +896,14 @@ export class IcqqEndpoint extends Client {
747
896
  throw new Error(`icqq 未找到待处理请求: ${id}`);
748
897
  }
749
898
  if (target.request_type === 'friend') {
750
- await this.setFriendAddRequest(target.flag, true, remark);
899
+ await this.client.setFriendAddRequest(target.flag, true, remark);
751
900
  }
752
901
  else {
753
- await this.setGroupAddRequest(target.flag, true);
902
+ await this.client.setGroupAddRequest(target.flag, true);
754
903
  }
755
904
  }
756
905
  async #rejectRequest(id, reason) {
757
- const messages = await this.getSystemMsg();
906
+ const messages = await this.client.getSystemMsg();
758
907
  const target = messages.find((m) => {
759
908
  const msg = m;
760
909
  return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
@@ -763,10 +912,10 @@ export class IcqqEndpoint extends Client {
763
912
  throw new Error(`icqq 未找到待处理请求: ${id}`);
764
913
  }
765
914
  if (target.request_type === 'friend') {
766
- await this.setFriendAddRequest(target.flag, false);
915
+ await this.client.setFriendAddRequest(target.flag, false);
767
916
  }
768
917
  else {
769
- await this.setGroupAddRequest(target.flag, false, reason);
918
+ await this.client.setGroupAddRequest(target.flag, false, reason);
770
919
  }
771
920
  }
772
921
  }
@@ -1 +1 @@
1
- export declare const icqqEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
1
+ export declare const icqqEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage, string | undefined>>>;
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type IcqqAdapterConfig, type IcqqInboundMessage, type IcqqWireSegment, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
2
2
  export { IcqqEndpoint, type IcqqEndpointOptions, } from './endpoint.js';
3
3
  export * from './types.js';
4
- export { getIcqqAgentDeps, registerIcqqAgentEndpoint, setIcqqAgentDeps, type IcqqAgentDeps, type IcqqAgentEndpoint, } from './icqq-agent-deps.js';
4
+ export { icqqClient, type IcqqClient, type IcqqClientEventMap, } from './client.js';
5
5
  export { resolveIcqqInboundMessageId, } from './icqq-inbound.js';
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
2
2
  export { IcqqEndpoint, } from './endpoint.js';
3
3
  export * from './types.js';
4
- export { getIcqqAgentDeps, registerIcqqAgentEndpoint, setIcqqAgentDeps, } from './icqq-agent-deps.js';
4
+ export { icqqClient, } from './client.js';
5
5
  export { resolveIcqqInboundMessageId, } from './icqq-inbound.js';