@zhin.js/adapter-icqq 1.1.17 → 1.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +55 -10
  3. package/adapters/icqq/index.js +3 -19
  4. package/adapters/icqq/index.ts +2 -25
  5. package/commands/icqq/endpoint/definition.js +3 -25
  6. package/commands/icqq/endpoint/definition.ts +3 -24
  7. package/commands/icqq/login/code/index.js +28 -0
  8. package/commands/icqq/login/code/index.ts +30 -0
  9. package/commands/icqq/login/confirm/index.js +20 -0
  10. package/commands/icqq/login/confirm/index.ts +21 -0
  11. package/commands/icqq/login/definition.js +65 -0
  12. package/commands/icqq/login/definition.ts +72 -0
  13. package/commands/icqq/login/qr/index.js +53 -0
  14. package/commands/icqq/login/qr/index.ts +55 -0
  15. package/commands/icqq/login/retry/index.js +21 -0
  16. package/commands/icqq/login/retry/index.ts +23 -0
  17. package/commands/icqq/login/status/index.js +31 -0
  18. package/commands/icqq/login/status/index.ts +34 -0
  19. package/lib/adapter.d.ts +13 -0
  20. package/lib/adapter.js +27 -0
  21. package/lib/client.d.ts +6 -9
  22. package/lib/client.js +1 -3
  23. package/lib/console-spec.d.ts +17 -0
  24. package/lib/console-spec.js +21 -0
  25. package/lib/console.d.ts +2 -0
  26. package/lib/console.js +13 -0
  27. package/lib/endpoint.d.ts +12 -15
  28. package/lib/endpoint.js +373 -164
  29. package/lib/group-moderation.d.ts +6 -3
  30. package/lib/group-moderation.js +9 -8
  31. package/lib/group-scene.d.ts +3 -3
  32. package/lib/group-scene.js +8 -7
  33. package/lib/icqq-login-assist.d.ts +3 -1
  34. package/lib/icqq-login-assist.js +3 -1
  35. package/lib/icqq-login-control.d.ts +24 -0
  36. package/lib/icqq-login-control.js +25 -0
  37. package/lib/index.d.ts +3 -2
  38. package/lib/index.js +2 -2
  39. package/lib/protocol.d.ts +13 -0
  40. package/lib/protocol.js +24 -9
  41. package/package.json +25 -17
  42. package/pages/index/index.js +15 -0
  43. package/pages/index/index.ts +16 -0
  44. package/plugin.js +3 -5
  45. package/schema.json +174 -0
  46. package/src/adapter.ts +29 -0
  47. package/src/client.ts +6 -11
  48. package/src/console-spec.ts +26 -0
  49. package/src/console.ts +17 -0
  50. package/src/endpoint.ts +381 -178
  51. package/src/group-moderation.ts +10 -10
  52. package/src/group-scene.ts +8 -8
  53. package/src/icqq-login-assist.ts +6 -2
  54. package/src/icqq-login-control.ts +42 -0
  55. package/src/index.ts +4 -1
  56. package/src/protocol.ts +35 -8
  57. package/lib/icqq-runtime-state.d.ts +0 -1
  58. package/lib/icqq-runtime-state.js +0 -6
  59. package/src/icqq-runtime-state.ts +0 -7
package/lib/endpoint.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parseGroupMessageId, } from '@icqqjs/icqq';
2
2
  import { inspect } from 'node:util';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
- import { Endpoint, } from 'zhin.js/adapter';
4
+ import { createEndpointSpecification, endpointSpec, createEndpointOperation, } from 'zhin.js/adapter';
5
5
  import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments } from '@zhin.js/core';
6
6
  import { EndpointDeliveryError } from '@zhin.js/im-contract';
7
7
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
@@ -11,7 +11,7 @@ import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned,
11
11
  import { icqqNoticeDedupeKey, isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
12
12
  import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
13
13
  import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
14
- import { formatInboundContent, formatOutboundMessages, normalizeIcqqImageMedia, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
14
+ import { formatInboundContent, formatOutboundMessageParts, normalizeIcqqImageMedia, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
15
15
  import { ManagedIcqqClient } from './managed-client.js';
16
16
  import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
17
17
  import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
@@ -33,8 +33,8 @@ const BOUND_EVENTS = [
33
33
  'system.login.error',
34
34
  'system.login.auth',
35
35
  ];
36
- export class IcqqEndpoint extends Endpoint {
37
- client;
36
+ export class IcqqEndpoint extends ManagedIcqqClient {
37
+ [endpointSpec];
38
38
  #logger;
39
39
  #options;
40
40
  endpointName;
@@ -55,63 +55,79 @@ export class IcqqEndpoint extends Endpoint {
55
55
  #inflightInbound = new Set();
56
56
  #inboundOwner = new AsyncLocalStorage();
57
57
  #contentResolver;
58
- content;
59
- management = Object.freeze({
60
- inspectGroupMember: (group, user, signal) => inspectGroupMember(this.client, group, user, signal),
61
- inspectGroupScene: (group, signal) => inspectGroupScene(this.client, group, signal),
62
- inspectGroupMemberProfile: (group, user, signal) => inspectGroupMemberProfile(this.client, group, user, signal),
63
- moderateGroupMember: (group, user, action, signal) => moderateGroupMember(this.client, group, user, action, signal, () => this.#open && !this.#retiring && !this.#stopped),
64
- listFriends: async () => Array.from(this.client.fl.values()).map((f) => ({
58
+ #content;
59
+ loginControl;
60
+ #reloginTimer;
61
+ #reloginAttempts = 0;
62
+ #reloginInFlight;
63
+ #offlineNoticeAt = 0;
64
+ #challengeNoticeAt = 0;
65
+ #management = Object.freeze({
66
+ inspectGroupMember: request => inspectGroupMember(this, request),
67
+ inspectGroupScene: request => inspectGroupScene(this, request),
68
+ inspectGroupMemberProfile: request => inspectGroupMemberProfile(this, request),
69
+ moderateGroupMember: request => moderateGroupMember({ client: this, isActive: () => this.#open && !this.#retiring && !this.#stopped }, request),
70
+ listFriends: async (request = {}) => createEndpointOperation(request).run(() => Array.from(this.fl.values()).map((f) => ({
65
71
  user_id: f.user_id,
66
72
  nickname: f.nickname,
67
73
  remark: f.remark ?? '',
68
- })),
69
- listGroups: async () => Array.from(this.client.gl.values()).map((g) => ({
74
+ }))),
75
+ listGroups: async (request = {}) => createEndpointOperation(request).run(() => Array.from(this.gl.values()).map((g) => ({
70
76
  group_id: g.group_id,
71
77
  name: g.group_name,
72
- })),
73
- listChannels: async () => {
74
- await this.#guildCatalog.syncAll(this.client);
75
- return this.#guildCatalog.getGuildChannelList();
78
+ }))),
79
+ listChannels: async (request = {}) => {
80
+ const operation = createEndpointOperation(request);
81
+ await operation.run(() => this.#guildCatalog.syncAll(this));
82
+ return operation.run(() => this.#guildCatalog.getGuildChannelList());
76
83
  },
77
- listGroupMembers: async (groupId) => Array.from((await this.client.getGroupMemberList(Number(groupId))).values()),
78
- listRequests: () => this.#listPendingRequests(),
79
- approveRequest: (requestId, remark) => this.#approveRequest(requestId, remark),
80
- rejectRequest: (requestId, reason) => this.#rejectRequest(requestId, reason),
81
- kickGroupMember: async (groupId, userId) => {
82
- await this.client.setGroupKick(Number(groupId), Number(userId));
84
+ listGroupMembers: async ({ groupId, signal }) => Array.from((await createEndpointOperation({ signal }).run(() => this.getGroupMemberList(Number(groupId)))).values()),
85
+ listRequests: (request = {}) => createEndpointOperation(request).run(() => this.#listPendingRequests()),
86
+ approveRequest: request => this.#approveRequest(request),
87
+ rejectRequest: request => this.#rejectRequest(request),
88
+ kickGroupMember: async ({ groupId, userId, signal }) => {
89
+ const operation = createEndpointOperation({ signal });
90
+ await operation.run(() => this.setGroupKick(Number(groupId), Number(userId)));
83
91
  },
84
- muteGroupMember: async (groupId, userId, duration) => {
85
- await this.client.setGroupBan(Number(groupId), Number(userId), duration);
92
+ muteGroupMember: async ({ groupId, userId, durationSeconds: duration, signal }) => {
93
+ const operation = createEndpointOperation({ signal });
94
+ await operation.run(() => this.setGroupBan(Number(groupId), Number(userId), duration));
86
95
  },
87
- setGroupAdmin: async (groupId, userId, enabled) => {
88
- await this.client.setGroupAdmin(Number(groupId), Number(userId), enabled);
96
+ setGroupAdmin: async ({ groupId, userId, enabled, signal }) => {
97
+ const operation = createEndpointOperation({ signal });
98
+ await operation.run(() => this.setGroupAdmin(Number(groupId), Number(userId), enabled));
89
99
  },
90
- deleteFriend: async (userId) => {
91
- await this.client.deleteFriend(Number(userId));
100
+ deleteFriend: async ({ userId, signal }) => {
101
+ const operation = createEndpointOperation({ signal });
102
+ await operation.run(() => this.deleteFriend(Number(userId)));
92
103
  },
93
- sendGroupNotice: async (groupId, content) => {
94
- if (await this.client.sendGroupNotice(Number(groupId), content) === false) {
104
+ sendGroupNotice: async ({ groupId, content, signal }) => {
105
+ const operation = createEndpointOperation({ signal });
106
+ if (await operation.run(() => this.sendGroupNotice(Number(groupId), content)) === false) {
95
107
  throw new Error('QQ did not confirm the group notice');
96
108
  }
97
109
  },
98
110
  });
99
- control = Object.freeze({
100
- recall: async (message) => {
111
+ #control = Object.freeze({
112
+ recall: async ({ message, signal }) => {
101
113
  if (!message.id || message.id.startsWith('outbound:'))
102
114
  return;
103
- if (!await this.client.deleteMsg(message.id))
115
+ if (!await createEndpointOperation({ signal }).run(() => this.deleteMsg(message.id)))
104
116
  throw new Error('QQ did not confirm message recall');
105
117
  },
106
- addReaction: async (message, emoji) => {
118
+ addReaction: async ({ message, emoji, signal }) => {
107
119
  const target = resolveIcqqGroupReactionTarget(message);
108
120
  if (!target)
109
121
  return null;
110
122
  try {
111
123
  // Do not await protocol ACK — packet timeout must not stall the AI reply.
112
- this.#ignoreReactionAck('add', this.client.pickGroup(target.groupId).setReaction(target.seq, emoji));
124
+ await createEndpointOperation({ signal }).run(() => {
125
+ this.#ignoreReactionAck('add', this.pickGroup(target.groupId).setReaction(target.seq, emoji));
126
+ });
113
127
  }
114
128
  catch (error) {
129
+ if (error instanceof EndpointDeliveryError && error.code === 'endpoint_inactive')
130
+ throw error;
115
131
  this.#logger.debug(formatCompact({
116
132
  op: 'reaction_add_failed',
117
133
  error: error instanceof Error ? error.message : String(error),
@@ -119,14 +135,18 @@ export class IcqqEndpoint extends Endpoint {
119
135
  }
120
136
  return emoji;
121
137
  },
122
- removeReaction: async (message, reactionId) => {
138
+ removeReaction: async ({ message, reactionId, signal }) => {
123
139
  const target = resolveIcqqGroupReactionTarget(message);
124
140
  if (!target)
125
141
  return;
126
142
  try {
127
- this.#ignoreReactionAck('remove', this.client.pickGroup(target.groupId).delReaction(target.seq, reactionId));
143
+ await createEndpointOperation({ signal }).run(() => {
144
+ this.#ignoreReactionAck('remove', this.pickGroup(target.groupId).delReaction(target.seq, reactionId));
145
+ });
128
146
  }
129
147
  catch (error) {
148
+ if (error instanceof EndpointDeliveryError && error.code === 'endpoint_inactive')
149
+ throw error;
130
150
  this.#logger.debug(formatCompact({
131
151
  op: 'reaction_remove_failed',
132
152
  error: error instanceof Error ? error.message : String(error),
@@ -135,14 +155,35 @@ export class IcqqEndpoint extends Endpoint {
135
155
  },
136
156
  });
137
157
  constructor(options) {
138
- super();
139
- const nativeConfig = resolveNativeClientConfig(options.config);
140
- this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig, (operation, error) => this.#handleBackgroundFailure(operation, error));
141
- this.#contentResolver = new IcqqContentResolver(this.client);
142
- this.content = this.#contentResolver.port;
158
+ super(Number(options.config.id), resolveNativeClientConfig(options.config), (operation, error) => this.#handleBackgroundFailure(operation, error));
159
+ const name = () => this.endpointName;
160
+ const state = () => this.#transportState;
161
+ const content = () => this.#content;
162
+ this[endpointSpec] = createEndpointSpecification(this, {
163
+ start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(), stop: () => this.#stopFramework(),
164
+ send: request => this.#sendFramework(request), get name() { return name(); },
165
+ get transportState() { return state(); },
166
+ management: this.#management, control: this.#control,
167
+ get content() { return content(); },
168
+ });
169
+ this.#contentResolver = new IcqqContentResolver(this);
170
+ this.#content = this.#contentResolver.port;
143
171
  this.#logger = getAdapterLogger('icqq', options.config.id);
144
172
  this.#options = options;
145
173
  this.endpointName = options.config.id;
174
+ this.loginControl = Object.freeze({
175
+ endpointId: this.endpointName,
176
+ state: () => ({
177
+ endpointId: this.endpointName,
178
+ transportState: this.#transportState,
179
+ online: this.isOnline(),
180
+ pending: this.#pendingLoginTasks(),
181
+ }),
182
+ pending: () => this.#pendingLoginTasks(),
183
+ relogin: async () => { this.#options.signal?.throwIfAborted(); await this.relogin(); },
184
+ submit: (taskId, value) => !this.#options.signal?.aborted && this.#options.loginAssist.submit(taskId, value, this.#options.endpointIdentity),
185
+ cancel: (taskId, reason) => !this.#options.signal?.aborted && this.#options.loginAssist.cancel(taskId, reason, this.#options.endpointIdentity),
186
+ });
146
187
  }
147
188
  #handleBackgroundFailure(operation, error) {
148
189
  if (!this.#started || this.#stopped || this.#retiring)
@@ -154,13 +195,162 @@ export class IcqqEndpoint extends Endpoint {
154
195
  : typeof error === 'string' ? error : 'Unknown ICQQ SDK background failure',
155
196
  ...(error && typeof error === 'object' && 'code' in error && typeof error.code === 'number' ? { code: error.code } : {}),
156
197
  }));
157
- if (operation === 'sso-heartbeat' && !this.client.isOnline())
198
+ if (operation === 'sso-heartbeat' && !this.isOnline())
158
199
  return;
159
200
  if (operation === 'login' && !this.#everOnline)
160
201
  return;
161
202
  // Delegate to the SDK's existing transport termination, offline event and
162
203
  // reconn_interval policy. No account-level logout or second retry timer.
163
- this.client.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
204
+ this.emit('internal.error.network', -2, `ICQQ ${operation} failed`);
205
+ }
206
+ /** Force a login attempt (manual recovery command). */
207
+ relogin() {
208
+ return this.#startRelogin(true);
209
+ }
210
+ #startRelogin(resetAttempts) {
211
+ if (this.#stopped || this.#retiring) {
212
+ return Promise.reject(new Error('ICQQ endpoint 已停止'));
213
+ }
214
+ if (this.#reloginInFlight)
215
+ return this.#reloginInFlight;
216
+ if (resetAttempts)
217
+ this.#reloginAttempts = 0;
218
+ const task = this.#performRelogin();
219
+ this.#reloginInFlight = task;
220
+ void task.catch(() => undefined).finally(() => {
221
+ if (this.#reloginInFlight === task)
222
+ this.#reloginInFlight = undefined;
223
+ if (this.#started && !this.#stopped && !this.#retiring && !this.isOnline()) {
224
+ this.#scheduleRelogin('relogin_failed');
225
+ }
226
+ });
227
+ return task;
228
+ }
229
+ async #performRelogin() {
230
+ this.#clearReloginTimer();
231
+ this.cancelReconnect();
232
+ this.#logger.info(formatCompact({ op: 'icqq_relogin', endpoint: this.endpointName }));
233
+ try {
234
+ await this.login(this.#options.config.password);
235
+ }
236
+ catch (error) {
237
+ this.#logger.warn(formatCompact({
238
+ op: 'icqq_relogin_failed',
239
+ endpoint: this.endpointName,
240
+ error: error instanceof Error ? error.message : String(error),
241
+ }));
242
+ throw error;
243
+ }
244
+ }
245
+ #scheduleRelogin(reason) {
246
+ if (!this.#options.config.autoReconnect || this.#stopped || this.#retiring)
247
+ return;
248
+ if (this.#reloginTimer || this.#reloginInFlight)
249
+ return;
250
+ if (this.#reloginAttempts >= 8)
251
+ return;
252
+ const attempt = this.#reloginAttempts++;
253
+ const delayMs = Math.min(300_000, 5_000 * (2 ** Math.min(attempt, 5)));
254
+ this.#logger.info(formatCompact({
255
+ op: 'icqq_relogin_scheduled',
256
+ endpoint: this.endpointName,
257
+ reason,
258
+ attempt: attempt + 1,
259
+ delay_ms: delayMs,
260
+ }));
261
+ this.#reloginTimer = setTimeout(() => {
262
+ this.#reloginTimer = undefined;
263
+ void this.#startRelogin(false).catch(() => {
264
+ // #performRelogin already scheduled the next attempt.
265
+ });
266
+ }, delayMs);
267
+ this.#reloginTimer.unref?.();
268
+ }
269
+ #clearReloginTimer() {
270
+ if (!this.#reloginTimer)
271
+ return;
272
+ clearTimeout(this.#reloginTimer);
273
+ this.#reloginTimer = undefined;
274
+ }
275
+ #pendingLoginTasks() {
276
+ if (this.#options.signal?.aborted)
277
+ return [];
278
+ return this.#options.loginAssist.listPending()
279
+ .filter((task) => task.adapter === 'icqq' && (this.#options.endpointIdentity
280
+ ? task.endpointIdentity?.canonicalId === this.#options.endpointIdentity.canonicalId
281
+ && task.endpointIdentity.instanceId === this.#options.endpointIdentity.instanceId
282
+ && task.endpointIdentity.epoch === this.#options.endpointIdentity.epoch
283
+ && task.endpointIdentity.incarnationId === this.#options.endpointIdentity.incarnationId
284
+ : task.endpointKey === this.endpointName));
285
+ }
286
+ #notifyOffline(reason, willRetry) {
287
+ const notifier = this.#options.notifier;
288
+ if (!notifier?.configured)
289
+ return;
290
+ const now = Date.now();
291
+ if (now - this.#offlineNoticeAt < 60_000)
292
+ return;
293
+ this.#offlineNoticeAt = now;
294
+ const recovery = willRetry
295
+ ? '已开始自动重新登录。'
296
+ : '请手动重新登录。';
297
+ const qrCommand = `icqq login qr ${this.endpointName}`;
298
+ const confirmCommand = `icqq login confirm ${this.endpointName}`;
299
+ const codeCommand = `icqq login code ${this.endpointName} <内容>`;
300
+ const text = `[Zhin] ICQQ ${this.endpointName} 掉线(${reason})。${recovery}\n`
301
+ + `恢复:在 QQ 回复 \`${qrCommand}\` 获取二维码,扫码后回复 \`${confirmCommand}\`;`
302
+ + `滑块/短信验证请回复 \`${codeCommand}\`。也可以打开 Console 的 ICQQ 页面完成登录。`;
303
+ void notifier.send(text).then((sent) => {
304
+ if (sent)
305
+ return;
306
+ this.#logger.warn(formatCompact({
307
+ op: 'icqq_notify_failed',
308
+ endpoint: this.endpointName,
309
+ reason,
310
+ }));
311
+ });
312
+ }
313
+ #notifyChallenge(step) {
314
+ const notifier = this.#options.notifier;
315
+ if (!notifier?.configured)
316
+ return;
317
+ const now = Date.now();
318
+ if (now - this.#challengeNoticeAt < 30_000)
319
+ return;
320
+ this.#challengeNoticeAt = now;
321
+ const label = step === 'qrcode' ? '二维码验证'
322
+ : step === 'slider' ? '滑块验证'
323
+ : step === 'device' ? '设备确认'
324
+ : '登录确认';
325
+ const qrCommand = `icqq login qr ${this.endpointName}`;
326
+ const confirmCommand = `icqq login confirm ${this.endpointName}`;
327
+ const codeCommand = `icqq login code ${this.endpointName} <内容>`;
328
+ const text = `[Zhin] ICQQ ${this.endpointName} 需要${label}。\n`
329
+ + (step === 'qrcode' || step === 'auth'
330
+ ? `在 QQ 回复 \`${qrCommand}\` 获取二维码,完成后回复 \`${confirmCommand}\`。`
331
+ : `在 QQ 回复 \`icqq login status ${this.endpointName}\` 查看验证链接,再用 \`${codeCommand}\` 提交验证码;也可以打开 Console 的 ICQQ 页面。`);
332
+ void notifier.send(text).then((sent) => {
333
+ if (sent)
334
+ return;
335
+ this.#logger.warn(formatCompact({
336
+ op: 'icqq_notify_failed',
337
+ endpoint: this.endpointName,
338
+ reason: `login_${step}`,
339
+ }));
340
+ });
341
+ }
342
+ #notifyRecovered() {
343
+ const notifier = this.#options.notifier;
344
+ const hadOfflineNotice = this.#offlineNoticeAt > 0;
345
+ this.#offlineNoticeAt = 0;
346
+ this.#challengeNoticeAt = 0;
347
+ if (!notifier?.configured || !hadOfflineNotice)
348
+ return;
349
+ void notifier.send(`[Zhin] ICQQ ${this.endpointName} 已重新上线。`).then((sent) => {
350
+ if (sent)
351
+ return;
352
+ this.#logger.warn(formatCompact({ op: 'icqq_notify_failed', endpoint: this.endpointName, reason: 'recovered' }));
353
+ });
164
354
  }
165
355
  #ignoreReactionAck(action, task) {
166
356
  void Promise.resolve(task).catch((error) => {
@@ -170,18 +360,18 @@ export class IcqqEndpoint extends Endpoint {
170
360
  }));
171
361
  });
172
362
  }
173
- get transportState() {
363
+ get #transportState() {
174
364
  if (this.#stopped || this.#retiring)
175
365
  return 'stopped';
176
366
  if (!this.#started)
177
367
  return 'idle';
178
- if (this.client.isOnline())
368
+ if (this.isOnline())
179
369
  return 'open';
180
370
  if (!this.#everOnline)
181
371
  return 'connecting';
182
372
  return this.#options.config.autoReconnect ? 'reconnecting' : 'closed';
183
373
  }
184
- async start(signal) {
374
+ async #startFramework(signal) {
185
375
  if (this.#started)
186
376
  return;
187
377
  signal.throwIfAborted();
@@ -191,7 +381,7 @@ export class IcqqEndpoint extends Endpoint {
191
381
  this.#retirementEpoch += 1;
192
382
  this.#retirementArmed = false;
193
383
  this.#retiring = false;
194
- this.client.off('system.online');
384
+ this.off('system.online');
195
385
  this.#started = true;
196
386
  let resolveOnline;
197
387
  const onlineReady = new Promise((resolve) => {
@@ -202,15 +392,16 @@ export class IcqqEndpoint extends Endpoint {
202
392
  signal.addEventListener('abort', onAbort, { once: true });
203
393
  try {
204
394
  this.#bindClientEvents();
205
- await raceAbort(Promise.resolve(this.client.login(this.#options.config.password)), signal);
395
+ await raceAbort(Promise.resolve(this.login(this.#options.config.password)), signal);
206
396
  await onlineReady;
207
397
  signal.throwIfAborted();
208
398
  await this.#pullPendingSystemMessages(signal);
209
399
  signal.throwIfAborted();
210
- this.#logger.info(`connected (direct) | friends: ${this.client.fl.size} | groups: ${this.client.gl.size}`);
400
+ this.#logger.info(`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`);
211
401
  }
212
402
  catch (error) {
213
- await this.stop();
403
+ this.#notifyOffline('start_failed', false);
404
+ await this.#stopFramework();
214
405
  this.#logger.debug(`Failed to connect ICQQ client: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
215
406
  throw error;
216
407
  }
@@ -220,11 +411,11 @@ export class IcqqEndpoint extends Endpoint {
220
411
  this.#resolveStartOnline = undefined;
221
412
  }
222
413
  }
223
- open() {
414
+ #openFramework() {
224
415
  if (this.#retirementArmed) {
225
416
  this.#retirementEpoch += 1;
226
417
  this.#retirementArmed = false;
227
- this.client.off('system.online');
418
+ this.off('system.online');
228
419
  this.#bindSystemOnlineEvent();
229
420
  }
230
421
  this.#retiring = false;
@@ -236,14 +427,15 @@ export class IcqqEndpoint extends Endpoint {
236
427
  });
237
428
  }
238
429
  }
239
- async close() {
430
+ async #closeFramework() {
240
431
  this.#assertExternalLifecycle('close');
241
432
  this.#armRetirementFence();
242
433
  await Promise.allSettled([...this.#inflightInbound]);
243
434
  }
244
- async stop() {
245
- this.#stopped = true;
435
+ async #stopFramework() {
246
436
  this.#assertExternalLifecycle('stop');
437
+ this.#stopped = true;
438
+ this.#clearReloginTimer();
247
439
  this.#armRetirementFence();
248
440
  await Promise.allSettled([...this.#inflightInbound]);
249
441
  this.#resolveStartOnline?.();
@@ -255,7 +447,7 @@ export class IcqqEndpoint extends Endpoint {
255
447
  // The retirement guard owns this matcher until a late login handshake
256
448
  // has finished. start()/open() replace it with the normal listener.
257
449
  if (event !== 'system.online')
258
- this.client.off(event);
450
+ this.off(event);
259
451
  }
260
452
  // `Client.logout()` sends an account-level unregister packet before it
261
453
  // closes this transport. During config HMR the replacement endpoint for
@@ -263,7 +455,7 @@ export class IcqqEndpoint extends Endpoint {
263
455
  // also closes the replacement session. Endpoint ownership is transport
264
456
  // scoped: retire only this Client's TCP connection.
265
457
  try {
266
- this.client.terminate();
458
+ this.terminate();
267
459
  }
268
460
  catch { /* ignore */ }
269
461
  this.#inboundDeduper.clear();
@@ -281,38 +473,49 @@ export class IcqqEndpoint extends Endpoint {
281
473
  const retirementEpoch = ++this.#retirementEpoch;
282
474
  // ICQQ owns reconnect through this per-Client timer. Clear it before any
283
475
  // asynchronous drain so a queued callback cannot enter while close waits.
284
- this.client.cancelReconnect();
476
+ this.cancelReconnect();
285
477
  // A login callback may already be inside the SDK handshake, which has no
286
478
  // abort API. Replace the normal online listener with an epoch-owned guard
287
479
  // before draining inbound work. Transport termination is account-local;
288
480
  // logout would also unregister an HMR replacement using the same UIN.
289
- this.client.off('system.online');
290
- this.client.on('system.online', () => {
481
+ this.off('system.online');
482
+ this.on('system.online', () => {
291
483
  if (this.#retirementEpoch !== retirementEpoch || !this.#retiring)
292
484
  return;
293
485
  try {
294
- this.client.terminate();
486
+ this.terminate();
295
487
  }
296
488
  catch { /* already closed */ }
297
489
  });
298
490
  }
299
- async send({ conversation, payload }) {
491
+ async #sendFramework({ conversation, payload, signal }) {
300
492
  if (!this.#started)
301
493
  throw new EndpointDeliveryError('endpoint_disconnected', 'icqq endpoint 未连接', 'not_sent');
494
+ const operation = createEndpointOperation({ signal });
302
495
  const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
303
- const prepared = isIcqqSegmentPayload(payload)
496
+ const prepared = await operation.prepare(() => isIcqqSegmentPayload(payload)
304
497
  ? prepareIcqqOutboundMedia(normalizeIcqqImageMedia(payload), mediaMode)
305
- : { content: payload, dispose() { } };
498
+ : { content: payload, dispose() { } });
499
+ // Cancellation ends our wait, not the SDK's use of temporary media. The actual native
500
+ // promises own that resource until they settle, including a late rejection after cancellation.
501
+ const pendingSends = new Set();
502
+ const sendNative = (target, message) => {
503
+ const task = this.#sendNative(target, message);
504
+ pendingSends.add(task);
505
+ const settled = () => { pendingSends.delete(task); };
506
+ void task.then(settled, settled);
507
+ return task;
508
+ };
306
509
  try {
307
510
  // Standalone elements (share / file / …) are split into ordered messages.
308
- const messages = formatOutboundMessages(prepared.content);
511
+ const messages = formatOutboundMessageParts(prepared.content);
309
512
  const parsed = icqqOutboundTarget(conversation);
310
- let messageId;
311
- for (const [index, message] of messages.entries()) {
513
+ const parts = [];
514
+ for (const [index, { message, sourceIndices }] of messages.entries()) {
312
515
  const preview = formatIcqqSendablePreview(message);
313
516
  let result;
314
517
  try {
315
- result = await this.#sendNative(parsed, message);
518
+ result = await operation.run(() => sendNative(parsed, message));
316
519
  }
317
520
  catch (error) {
318
521
  this.#logger.warn(formatCompact({
@@ -324,66 +527,55 @@ export class IcqqEndpoint extends Endpoint {
324
527
  }));
325
528
  if (index === 0)
326
529
  throw error;
327
- // Earlier parts are already delivered: a retry would duplicate them.
328
- throw new EndpointDeliveryError('partial_delivery', `ICQQ delivered ${index} of ${messages.length} message parts`, 'unknown', { cause: error });
530
+ // Earlier sends completed; retrying can duplicate accepted messages, even without ids.
531
+ throw new EndpointDeliveryError('partial_delivery', `ICQQ completed ${index} of ${messages.length} message sends`, 'unknown', { cause: error, parts });
329
532
  }
330
533
  const rawId = result?.message_id;
331
534
  const id = (typeof rawId === 'string' || (typeof rawId === 'number' && Number.isFinite(rawId))) && String(rawId).trim() !== ''
332
535
  ? String(rawId) : undefined;
333
- messageId ??= id;
536
+ parts.push({ ...(id ? { messageId: id } : {}), ...(sourceIndices ? { sourceIndices } : {}) });
334
537
  this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${id ?? '-'} | ${truncatePreview(preview, 80)}`);
335
538
  }
336
- // Native share / file APIs return no id; the reply is confirmed by any part that has one.
337
- if (!messageId) {
338
- throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown');
539
+ // A resolved share/file call has no message identity; retain that unknown slot in the batch.
540
+ if (!parts.some(part => part.messageId)) {
541
+ throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown', { parts });
339
542
  }
340
- return messageId;
543
+ return { parts };
341
544
  }
342
545
  finally {
343
- prepared.dispose();
344
- }
345
- }
346
- /**
347
- * ICQQ's share API returns no message id; a resolved call only means the server accepted it.
348
- * The card is the bot's own newest card message in the conversation history, so read the id
349
- * from there (once more after a short wait, history can lag). Undefined keeps the delivery
350
- * unconfirmed rather than inventing an id.
351
- */
352
- async #findOwnCard(history, sentAt) {
353
- const self = Number(this.client.uin);
354
- for (const wait of [0, 500]) {
355
- if (wait)
356
- await new Promise(resolve => setTimeout(resolve, wait));
357
- // A missing or throwing history API must not turn an accepted share into a send failure.
358
- const messages = await Promise.resolve().then(history).catch(() => []);
359
- const card = [...messages].reverse().find(item => Number(item.sender?.user_id ?? item.user_id) === self
360
- && (item.time ?? 0) >= sentAt - 2
361
- && (item.message ?? []).some(element => element.type === 'json' || element.type === 'xml' || element.type === 'share'));
362
- if (card && card.message_id != null && String(card.message_id).trim())
363
- return String(card.message_id);
546
+ const disposeMedia = () => {
547
+ try {
548
+ prepared.dispose();
549
+ }
550
+ catch (error) {
551
+ // Temporary-file cleanup cannot change an observed send result.
552
+ this.#logger.warn(formatCompact({ op: 'icqq_media_cleanup_failed', error: describeUnknownError(error) }));
553
+ }
554
+ };
555
+ if (pendingSends.size)
556
+ void Promise.allSettled([...pendingSends]).then(disposeMedia);
557
+ else
558
+ disposeMedia();
364
559
  }
365
- return undefined;
366
560
  }
367
561
  async #sendNative(target, message) {
368
562
  if (isIcqqShareElement(message)) {
369
563
  switch (target.kind) {
370
564
  case 'private': {
371
- const sentAt = Math.floor(Date.now() / 1000);
372
- const friend = this.client.pickFriend(target.userId);
565
+ const friend = this.pickFriend(target.userId);
373
566
  await friend.share(message);
374
- return { message_id: await this.#findOwnCard(() => friend.getChatHistory(), sentAt) };
567
+ return {};
375
568
  }
376
569
  case 'group': {
377
- const sentAt = Math.floor(Date.now() / 1000);
378
- const group = this.client.pickGroup(target.groupId);
570
+ const group = this.pickGroup(target.groupId);
379
571
  await group.share(message);
380
- return { message_id: await this.#findOwnCard(() => group.getChatHistory(), sentAt) };
572
+ return {};
381
573
  }
382
574
  case 'temp':
383
- await this.client.pickMember(target.groupId, target.userId).share(message);
575
+ await this.pickMember(target.groupId, target.userId).share(message);
384
576
  return {};
385
577
  case 'channel': {
386
- const channel = this.client.pickGuild(target.guildId).channels.get(target.channelId);
578
+ const channel = this.pickGuild(target.guildId).channels.get(target.channelId);
387
579
  if (!channel) {
388
580
  throw new TypeError(`ICQQ guild channel is unavailable: ${target.guildId}/${target.channelId}`);
389
581
  }
@@ -395,11 +587,11 @@ export class IcqqEndpoint extends Endpoint {
395
587
  if (isIcqqFileElement(message)) {
396
588
  switch (target.kind) {
397
589
  case 'private': {
398
- await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
590
+ await this.pickFriend(target.userId).sendFile(message.file, message.name);
399
591
  return {};
400
592
  }
401
593
  case 'group': {
402
- await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
594
+ await this.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
403
595
  return {};
404
596
  }
405
597
  case 'temp':
@@ -409,13 +601,13 @@ export class IcqqEndpoint extends Endpoint {
409
601
  }
410
602
  switch (target.kind) {
411
603
  case 'private':
412
- return this.client.sendPrivateMsg(target.userId, message);
604
+ return this.sendPrivateMsg(target.userId, message);
413
605
  case 'group':
414
- return this.client.sendGroupMsg(target.groupId, message);
606
+ return this.sendGroupMsg(target.groupId, message);
415
607
  case 'temp':
416
- return this.client.sendTempMsg(target.groupId, target.userId, message);
608
+ return this.sendTempMsg(target.groupId, target.userId, message);
417
609
  case 'channel':
418
- return this.client.sendGuildMsg(target.guildId, target.channelId, message);
610
+ return this.sendGuildMsg(target.guildId, target.channelId, message);
419
611
  }
420
612
  }
421
613
  admit(msg) {
@@ -428,7 +620,7 @@ export class IcqqEndpoint extends Endpoint {
428
620
  this.#inflightInbound.add(settlement);
429
621
  let operation;
430
622
  try {
431
- operation = this.#inboundOwner.run(owner, run);
623
+ operation = this.#inboundOwner.run(owner, () => this[endpointSpec].events.run(run));
432
624
  }
433
625
  catch (error) {
434
626
  this.#inflightInbound.delete(settlement);
@@ -477,7 +669,9 @@ export class IcqqEndpoint extends Endpoint {
477
669
  + (msg.sender ? ` from ${msg.sender.name ?? msg.sender.id}` : '')
478
670
  + (mentioned ? ' (mentioned)' : '')
479
671
  + ` | ${truncatePreview(msg.content, 80)}`);
480
- await this.emit('message.receive', {
672
+ await this[endpointSpec].events.emit('message.receive', {
673
+ event: msg.event,
674
+ platformSentAt: Number.isFinite(msg.platformSentAt) ? msg.platformSentAt : undefined,
481
675
  conversation,
482
676
  message: { conversation, id: msg.id },
483
677
  content: msg.content,
@@ -503,80 +697,81 @@ export class IcqqEndpoint extends Endpoint {
503
697
  #bindClientEvents() {
504
698
  this.#bindNativeEvents();
505
699
  const safe = (label, fn) => {
506
- try {
507
- fn();
508
- }
509
- catch (error) {
510
- this.#logger.warn(formatCompact({
511
- op: 'icqq_event_handler',
512
- endpoint: this.endpointName,
513
- event: label,
514
- error: error instanceof Error ? error.message : String(error),
515
- }));
516
- }
700
+ void this[endpointSpec].events.run(fn).catch(() => {
701
+ this.#logger.debug(formatCompact({ op: 'icqq_event_not_admitted', endpoint: this.endpointName, event: label }));
702
+ });
517
703
  };
518
- this.client.on('message.private', (event) => {
704
+ this.on('message.private', (event) => {
519
705
  this.#startMessageEvent('message.private', event);
520
706
  });
521
- this.client.on('message.group', (event) => {
707
+ this.on('message.group', (event) => {
522
708
  this.#startMessageEvent('message.group', event);
523
709
  });
524
- this.client.on('message.guild', (event) => {
525
- void this.#handleGuildEvent(event).catch((error) => {
710
+ this.on('message.guild', (event) => {
711
+ void this.#trackInbound(() => this.#handleGuildEvent(event)).catch((error) => {
526
712
  this.#logInboundHandlerFailure('message.guild', error);
527
713
  });
528
714
  });
529
- this.client.on('request.friend', (event) => safe('request.friend', () => {
715
+ this.on('request.friend', (event) => safe('request.friend', () => {
530
716
  this.#onRequestEvent(serializeIcqqEvent(event));
531
717
  }));
532
- this.client.on('request.group', (event) => safe('request.group', () => {
718
+ this.on('request.group', (event) => safe('request.group', () => {
533
719
  this.#onRequestEvent(serializeIcqqEvent(event));
534
720
  }));
535
- this.client.on('notice.friend', (event) => safe('notice.friend', () => {
721
+ this.on('notice.friend', (event) => safe('notice.friend', () => {
536
722
  this.#onNoticeEvent(serializeIcqqEvent(event));
537
723
  }));
538
- this.client.on('notice.group', (event) => safe('notice.group', () => {
724
+ this.on('notice.group', (event) => safe('notice.group', () => {
539
725
  this.#onNoticeEvent(serializeIcqqEvent(event));
540
726
  }));
541
727
  this.#bindSystemOnlineEvent();
542
- this.client.on('system.offline', (event) => {
728
+ this.on('system.offline', (event) => {
543
729
  this.#logSystemEvent('offline', event);
730
+ this.#notifyOffline('offline', this.#options.config.autoReconnect);
544
731
  });
545
- this.client.on('system.offline.network', (event) => {
732
+ this.on('system.offline.network', (event) => {
546
733
  this.#logSystemEvent('offline.network', event);
734
+ this.#notifyOffline('network', this.#options.config.autoReconnect);
547
735
  });
548
- this.client.on('system.offline.kickoff', (event) => {
736
+ this.on('system.offline.kickoff', (event) => {
549
737
  this.#logSystemEvent('offline.kickoff', event);
738
+ this.#notifyOffline('kickoff', this.#options.config.autoReconnect);
739
+ this.#scheduleRelogin('kickoff');
550
740
  });
551
- this.client.on('system.login.qrcode', (event) => {
741
+ this.on('system.login.qrcode', (event) => {
552
742
  this.#logger.info('icqq 收到二维码登录事件,等待 LoginAssist / 终端确认后继续');
553
743
  this.#handleLoginChallenge('qrcode', event);
554
744
  });
555
- this.client.on('system.login.device', (event) => {
745
+ this.on('system.login.device', (event) => {
556
746
  this.#handleLoginChallenge('device', event);
557
747
  });
558
- this.client.on('system.login.slider', (event) => {
748
+ this.on('system.login.slider', (event) => {
559
749
  this.#handleLoginChallenge('slider', event);
560
750
  });
561
- this.client.on('system.login.error', (event) => {
751
+ this.on('system.login.error', (event) => {
562
752
  this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'login_error');
563
753
  this.#logSystemEvent('login.error', event);
754
+ this.#notifyOffline('login_error', this.#options.config.autoReconnect);
755
+ this.#scheduleRelogin('login_error');
564
756
  });
565
- this.client.on('system.login.auth', (event) => {
757
+ this.on('system.login.auth', (event) => {
566
758
  this.#handleLoginChallenge('auth', event);
567
759
  });
568
760
  }
569
761
  #bindSystemOnlineEvent() {
570
- this.client.on('system.online', (event) => {
762
+ this.on('system.online', (event) => {
571
763
  if (this.#retiring)
572
764
  return;
573
765
  this.#everOnline = true;
766
+ this.#clearReloginTimer();
767
+ this.#reloginAttempts = 0;
768
+ this.#notifyRecovered();
574
769
  try {
575
770
  this.#resolveStartOnline?.();
576
771
  this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
577
772
  try {
578
773
  // transport 重连不清除(账号不变);stop/retire 由基类撤销登记。上报失败不影响上线流程。
579
- this.reportSelfUserId(resolveIcqqSelfUserId(this.client));
774
+ this[endpointSpec].reportSelfUserId(resolveIcqqSelfUserId(this));
580
775
  }
581
776
  catch (error) {
582
777
  this.#logger.warn(formatCompact({
@@ -601,7 +796,7 @@ export class IcqqEndpoint extends Endpoint {
601
796
  #bindNativeEvents() {
602
797
  this.#disposeNativeEvents?.();
603
798
  let matchedName;
604
- const registration = this.client.on((eventName) => {
799
+ const registration = this.on((eventName) => {
605
800
  matchedName = typeof eventName === 'string' && !eventName.startsWith('internal.')
606
801
  ? eventName
607
802
  : undefined;
@@ -614,7 +809,7 @@ export class IcqqEndpoint extends Endpoint {
614
809
  this.#disposeNativeEvents = () => registration();
615
810
  }
616
811
  #emitNativeEvent(name, event) {
617
- void this.emitPlatform(name, event).catch((error) => {
812
+ void this[endpointSpec].events.platform(name, event).catch((error) => {
618
813
  this.#logger.warn(formatCompact({
619
814
  op: 'icqq_platform_event_failed',
620
815
  endpoint: this.endpointName,
@@ -624,17 +819,22 @@ export class IcqqEndpoint extends Endpoint {
624
819
  });
625
820
  }
626
821
  #handleLoginChallenge(step, event) {
822
+ if (this.#retiring || this.#stopped || this.#options.signal?.aborted)
823
+ return;
627
824
  const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
628
825
  ? event
629
826
  : null);
630
827
  this.#logSystemEvent(`login.${step}`, event);
828
+ this.#clearReloginTimer();
829
+ this.#notifyChallenge(step);
631
830
  void runIcqqLoginAssistStep({
632
831
  assist: this.#options.loginAssist,
633
832
  locale: this.#options.locale,
634
- client: this.client,
833
+ client: this,
635
834
  adapter: 'icqq',
636
835
  endpointKey: this.endpointName,
637
836
  owner: this.#loginAssistOwner,
837
+ signal: this.#options.signal, endpointIdentity: this.#options.endpointIdentity,
638
838
  logger: this.#logger,
639
839
  step,
640
840
  event: payload ?? event,
@@ -653,14 +853,14 @@ export class IcqqEndpoint extends Endpoint {
653
853
  const postType = String(payload.post_type ?? '');
654
854
  const isRequest = postType === 'request' || postType.startsWith('request.');
655
855
  const flag = payload.flag != null ? String(payload.flag) : '';
656
- void receiveOneBotLikeSideEvent((name, payload) => this.emit(name, payload), {
856
+ void receiveOneBotLikeSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), {
657
857
  adapter: 'icqq',
658
858
  endpointKey: this.endpointName,
659
859
  platform: 'icqq',
660
860
  raw: payload,
661
861
  ...(isRequest ? {
662
- approve: (id, remark) => this.#approveRequest(flag || id, remark),
663
- reject: (id, reason) => this.#rejectRequest(flag || id, reason),
862
+ approve: (id, remark) => this.#approveRequest({ requestId: flag || id, remark }),
863
+ reject: (id, reason) => this.#rejectRequest({ requestId: flag || id, reason }),
664
864
  } : {}),
665
865
  }).catch((err) => {
666
866
  this.#logger.warn(formatCompact({
@@ -672,7 +872,7 @@ export class IcqqEndpoint extends Endpoint {
672
872
  });
673
873
  }
674
874
  #dispatchIcqqSystemSideEvent(type, payload) {
675
- void this.emit('system.receive', buildSystem(payload ?? {}, {
875
+ void this[endpointSpec].events.emit('system.receive', buildSystem(payload ?? {}, {
676
876
  id: `system:${this.endpointName}:${type}:${Date.now()}`,
677
877
  clientAdapter: 'icqq',
678
878
  endpointId: this.endpointName,
@@ -764,6 +964,8 @@ export class IcqqEndpoint extends Endpoint {
764
964
  }));
765
965
  }
766
966
  await this.#deliverInbound({
967
+ event,
968
+ platformSentAt: normalized.timestampMs,
767
969
  id: normalized.messageId,
768
970
  conversation,
769
971
  content: formatInboundContent(normalized.rawMessage),
@@ -794,13 +996,13 @@ export class IcqqEndpoint extends Endpoint {
794
996
  return null;
795
997
  if (data.message_type === 'group' || data.group_id != null) {
796
998
  const groupId = Number(data.group_id);
797
- return Number.isFinite(groupId) && groupId > 0 ? this.client.pickGroup(groupId) : null;
999
+ return Number.isFinite(groupId) && groupId > 0 ? this.pickGroup(groupId) : null;
798
1000
  }
799
1001
  const sourceGroupId = Number(data.sender?.group_id);
800
1002
  if (Number.isFinite(sourceGroupId) && sourceGroupId > 0) {
801
- return this.client.pickMember(sourceGroupId, userId);
1003
+ return this.pickMember(sourceGroupId, userId);
802
1004
  }
803
- return this.client.pickFriend(userId);
1005
+ return this.pickFriend(userId);
804
1006
  }
805
1007
  #logInboundHandlerFailure(event, error) {
806
1008
  this.#logger.warn(formatCompact({
@@ -834,6 +1036,9 @@ export class IcqqEndpoint extends Endpoint {
834
1036
  return;
835
1037
  }
836
1038
  await this.admit({
1039
+ event,
1040
+ platformSentAt: typeof normalized.raw.time === 'number' && Number.isFinite(normalized.raw.time)
1041
+ ? normalized.timestampMs : undefined,
837
1042
  id: normalized.messageId,
838
1043
  conversation: icqqInboundConversation(String(this.#options.id), {
839
1044
  channelType: 'channel',
@@ -866,7 +1071,7 @@ export class IcqqEndpoint extends Endpoint {
866
1071
  this.#dispatchIcqqSideEvent(payload);
867
1072
  }
868
1073
  async #listPendingRequests() {
869
- const messages = await this.client.getSystemMsg();
1074
+ const messages = await this.getSystemMsg();
870
1075
  const rows = [];
871
1076
  for (const raw of messages) {
872
1077
  const msg = raw;
@@ -894,7 +1099,7 @@ export class IcqqEndpoint extends Endpoint {
894
1099
  }
895
1100
  async #pullPendingSystemMessages(signal) {
896
1101
  try {
897
- const messages = await raceAbort(Promise.resolve(this.client.getSystemMsg()), signal);
1102
+ const messages = await raceAbort(Promise.resolve(this.getSystemMsg()), signal);
898
1103
  for (const raw of messages) {
899
1104
  signal.throwIfAborted();
900
1105
  const msg = raw;
@@ -924,8 +1129,9 @@ export class IcqqEndpoint extends Endpoint {
924
1129
  }));
925
1130
  }
926
1131
  }
927
- async #approveRequest(id, remark) {
928
- const messages = await this.client.getSystemMsg();
1132
+ async #approveRequest({ requestId: id, remark, signal }) {
1133
+ const operation = createEndpointOperation({ signal });
1134
+ const messages = await operation.run(() => this.getSystemMsg());
929
1135
  const target = messages.find((m) => {
930
1136
  const msg = m;
931
1137
  return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
@@ -933,15 +1139,17 @@ export class IcqqEndpoint extends Endpoint {
933
1139
  if (!target?.flag) {
934
1140
  throw new Error(`icqq 未找到待处理请求: ${id}`);
935
1141
  }
1142
+ const flag = target.flag;
936
1143
  if (target.request_type === 'friend') {
937
- await this.client.setFriendAddRequest(target.flag, true, remark);
1144
+ await operation.run(() => this.setFriendAddRequest(flag, true, remark));
938
1145
  }
939
1146
  else {
940
- await this.client.setGroupAddRequest(target.flag, true);
1147
+ await operation.run(() => this.setGroupAddRequest(flag, true));
941
1148
  }
942
1149
  }
943
- async #rejectRequest(id, reason) {
944
- const messages = await this.client.getSystemMsg();
1150
+ async #rejectRequest({ requestId: id, reason, signal }) {
1151
+ const operation = createEndpointOperation({ signal });
1152
+ const messages = await operation.run(() => this.getSystemMsg());
945
1153
  const target = messages.find((m) => {
946
1154
  const msg = m;
947
1155
  return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
@@ -949,11 +1157,12 @@ export class IcqqEndpoint extends Endpoint {
949
1157
  if (!target?.flag) {
950
1158
  throw new Error(`icqq 未找到待处理请求: ${id}`);
951
1159
  }
1160
+ const flag = target.flag;
952
1161
  if (target.request_type === 'friend') {
953
- await this.client.setFriendAddRequest(target.flag, false);
1162
+ await operation.run(() => this.setFriendAddRequest(flag, false));
954
1163
  }
955
1164
  else {
956
- await this.client.setGroupAddRequest(target.flag, false, reason);
1165
+ await operation.run(() => this.setGroupAddRequest(flag, false, reason));
957
1166
  }
958
1167
  }
959
1168
  }