@zhin.js/adapter-icqq 1.0.71 → 1.1.2

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 (122) hide show
  1. package/CHANGELOG.md +1216 -52
  2. package/PERMITS.md +24 -0
  3. package/README.md +123 -429
  4. package/adapters/icqq/index.js +31 -0
  5. package/adapters/icqq/index.ts +42 -0
  6. package/agents/icqq/agent.json +20 -0
  7. package/agents/icqq/boundaries.md +3 -0
  8. package/agents/icqq/conventions.md +3 -0
  9. package/agents/icqq/skills/icqq-directory/SKILL.md +20 -0
  10. package/agents/icqq/skills/icqq-directory/tools/friend_list/index.js +19 -0
  11. package/agents/icqq/skills/icqq-directory/tools/friend_list/index.ts +19 -0
  12. package/agents/icqq/skills/icqq-directory/tools/group_list/index.js +20 -0
  13. package/agents/icqq/skills/icqq-directory/tools/group_list/index.ts +20 -0
  14. package/agents/icqq/skills/icqq-directory/tools/group_member_info/index.js +26 -0
  15. package/agents/icqq/skills/icqq-directory/tools/group_member_info/index.ts +28 -0
  16. package/agents/icqq/skills/icqq-directory/tools/user_info/index.js +15 -0
  17. package/agents/icqq/skills/icqq-directory/tools/user_info/index.ts +14 -0
  18. package/agents/icqq/skills/icqq-group-admin/SKILL.md +25 -0
  19. package/agents/icqq/skills/icqq-group-admin/tools/announce/index.js +18 -0
  20. package/agents/icqq/skills/icqq-group-admin/tools/announce/index.ts +17 -0
  21. package/agents/icqq/skills/icqq-group-admin/tools/essence/index.js +24 -0
  22. package/agents/icqq/skills/icqq-group-admin/tools/essence/index.ts +22 -0
  23. package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.js +23 -0
  24. package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.ts +20 -0
  25. package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.js +27 -0
  26. package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.ts +23 -0
  27. package/agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.js +23 -0
  28. package/agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.ts +21 -0
  29. package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.js +25 -0
  30. package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.ts +22 -0
  31. package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.js +26 -0
  32. package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.ts +23 -0
  33. package/agents/icqq/skills/icqq-group-state/SKILL.md +17 -0
  34. package/agents/icqq/skills/icqq-group-state/tools/group_files/index.js +24 -0
  35. package/agents/icqq/skills/icqq-group-state/tools/group_files/index.ts +22 -0
  36. package/agents/icqq/skills/icqq-group-state/tools/list_muted/index.js +18 -0
  37. package/agents/icqq/skills/icqq-group-state/tools/list_muted/index.ts +17 -0
  38. package/agents/icqq/skills/icqq-interaction/SKILL.md +19 -0
  39. package/agents/icqq/skills/icqq-interaction/tools/group_sign/index.js +20 -0
  40. package/agents/icqq/skills/icqq-interaction/tools/group_sign/index.ts +18 -0
  41. package/agents/icqq/skills/icqq-interaction/tools/poke/index.js +17 -0
  42. package/agents/icqq/skills/icqq-interaction/tools/poke/index.ts +16 -0
  43. package/agents/icqq/skills/icqq-interaction/tools/send_user_like/index.js +26 -0
  44. package/agents/icqq/skills/icqq-interaction/tools/send_user_like/index.ts +22 -0
  45. package/agents/icqq/system.md +3 -0
  46. package/commands/icqq/endpoint/add/[[id]]/index.js +3 -0
  47. package/commands/icqq/endpoint/add/[[id]]/index.ts +3 -0
  48. package/commands/icqq/endpoint/definition.js +31 -0
  49. package/commands/icqq/endpoint/definition.ts +32 -0
  50. package/commands/icqq/endpoint/list/index.js +3 -0
  51. package/commands/icqq/endpoint/list/index.ts +3 -0
  52. package/commands/icqq/endpoint/remove/[id]/index.js +3 -0
  53. package/commands/icqq/endpoint/remove/[id]/index.ts +3 -0
  54. package/lib/client.d.ts +19 -0
  55. package/lib/client.js +3 -0
  56. package/lib/content-resolver.d.ts +10 -0
  57. package/lib/content-resolver.js +98 -0
  58. package/lib/cq-message.d.ts +14 -0
  59. package/lib/cq-message.js +106 -0
  60. package/lib/endpoint.d.ts +30 -0
  61. package/lib/endpoint.js +1003 -0
  62. package/lib/forward-msg.d.ts +6 -0
  63. package/lib/forward-msg.js +107 -0
  64. package/lib/icqq-guild.d.ts +57 -0
  65. package/lib/icqq-guild.js +109 -0
  66. package/lib/icqq-inbound.d.ts +165 -0
  67. package/lib/icqq-inbound.js +651 -0
  68. package/lib/icqq-inbox.d.ts +25 -0
  69. package/lib/icqq-inbox.js +129 -0
  70. package/lib/icqq-login-assist.d.ts +27 -0
  71. package/lib/icqq-login-assist.js +109 -0
  72. package/lib/icqq-runtime-state.d.ts +1 -0
  73. package/lib/icqq-runtime-state.js +6 -0
  74. package/lib/index.d.ts +5 -18
  75. package/lib/index.js +5 -124
  76. package/lib/outbound-media.d.ts +14 -0
  77. package/lib/outbound-media.js +76 -0
  78. package/lib/protocol.d.ts +258 -0
  79. package/lib/protocol.js +422 -0
  80. package/lib/types.d.ts +33 -13
  81. package/lib/types.js +0 -1
  82. package/package.json +86 -32
  83. package/plugin.js +19 -0
  84. package/prompt-sections/platform/index.js +16 -0
  85. package/prompt-sections/platform/index.ts +16 -0
  86. package/schema.json +168 -0
  87. package/src/client.ts +29 -0
  88. package/src/content-resolver.ts +131 -0
  89. package/src/cq-message.ts +113 -0
  90. package/src/endpoint.ts +1084 -0
  91. package/src/forward-msg.ts +112 -0
  92. package/src/icqq-guild.ts +154 -0
  93. package/src/icqq-inbound.ts +814 -0
  94. package/src/icqq-inbox.ts +147 -0
  95. package/src/icqq-login-assist.ts +132 -0
  96. package/src/icqq-runtime-state.ts +7 -0
  97. package/src/index.ts +30 -149
  98. package/src/outbound-media.ts +103 -0
  99. package/src/protocol.ts +551 -0
  100. package/src/types.ts +35 -13
  101. package/LICENSE +0 -21
  102. package/client/ICQQManagement.tsx +0 -139
  103. package/client/LoginAssistPanel.tsx +0 -293
  104. package/client/index.tsx +0 -11
  105. package/client/tsconfig.json +0 -7
  106. package/client/utils/api.ts +0 -20
  107. package/dist/index.js +0 -1
  108. package/lib/adapter.d.ts +0 -34
  109. package/lib/adapter.d.ts.map +0 -1
  110. package/lib/adapter.js +0 -396
  111. package/lib/adapter.js.map +0 -1
  112. package/lib/bot.d.ts +0 -46
  113. package/lib/bot.d.ts.map +0 -1
  114. package/lib/bot.js +0 -457
  115. package/lib/bot.js.map +0 -1
  116. package/lib/index.d.ts.map +0 -1
  117. package/lib/index.js.map +0 -1
  118. package/lib/types.d.ts.map +0 -1
  119. package/lib/types.js.map +0 -1
  120. package/skills/icqq/SKILL.md +0 -22
  121. package/src/adapter.ts +0 -448
  122. package/src/bot.ts +0 -521
@@ -0,0 +1,1003 @@
1
+ import { Client, parseGroupMessageId, } from '@icqqjs/icqq';
2
+ import { inspect } from 'node:util';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { Endpoint, } from 'zhin.js/adapter';
5
+ import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments } from '@zhin.js/core';
6
+ import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
7
+ import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
8
+ import { IcqqContentResolver } from './content-resolver.js';
9
+ import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, messageLookupFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveIcqqInboundMedia, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
10
+ import { isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
11
+ import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
12
+ import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
13
+ import { formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
14
+ const BOUND_EVENTS = [
15
+ 'message.private',
16
+ 'message.group',
17
+ 'message.guild',
18
+ 'request.friend',
19
+ 'request.group',
20
+ 'notice.friend',
21
+ 'notice.group',
22
+ 'system.online',
23
+ 'system.offline',
24
+ 'system.offline.network',
25
+ 'system.offline.kickoff',
26
+ 'system.login.qrcode',
27
+ 'system.login.device',
28
+ 'system.login.slider',
29
+ 'system.login.error',
30
+ 'system.login.auth',
31
+ ];
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;
42
+ #logger;
43
+ #options;
44
+ endpointName;
45
+ #guildCatalog = new IcqqGuildCatalog();
46
+ #inboundDeduper = new InboundMessageDeduper();
47
+ #sideEventDeduper = new InboundMessageDeduper();
48
+ #loginAssistOwner = Object.freeze({});
49
+ #open = false;
50
+ #retiring = false;
51
+ #started = false;
52
+ #retirementEpoch = 0;
53
+ #retirementArmed = false;
54
+ #disposeNativeEvents;
55
+ #resolveStartOnline;
56
+ #heldInbound = [];
57
+ #inflightInbound = new Set();
58
+ #inboundOwner = new AsyncLocalStorage();
59
+ #contentResolver;
60
+ content;
61
+ management = Object.freeze({
62
+ listFriends: async () => Array.from(this.client.fl.values()).map((f) => ({
63
+ user_id: f.user_id,
64
+ nickname: f.nickname,
65
+ remark: f.remark ?? '',
66
+ })),
67
+ listGroups: async () => Array.from(this.client.gl.values()).map((g) => ({
68
+ group_id: g.group_id,
69
+ name: g.group_name,
70
+ })),
71
+ listChannels: async () => {
72
+ await this.#guildCatalog.syncAll(this.client);
73
+ return this.#guildCatalog.getGuildChannelList();
74
+ },
75
+ listGroupMembers: async (groupId) => Array.from((await this.client.getGroupMemberList(Number(groupId))).values()),
76
+ listRequests: () => this.#listPendingRequests(),
77
+ approveRequest: (requestId, remark) => this.#approveRequest(requestId, remark),
78
+ rejectRequest: (requestId, reason) => this.#rejectRequest(requestId, reason),
79
+ kickGroupMember: async (groupId, userId) => {
80
+ await this.client.setGroupKick(Number(groupId), Number(userId));
81
+ },
82
+ muteGroupMember: async (groupId, userId, duration) => {
83
+ await this.client.setGroupBan(Number(groupId), Number(userId), duration);
84
+ },
85
+ setGroupAdmin: async (groupId, userId, enabled) => {
86
+ await this.client.setGroupAdmin(Number(groupId), Number(userId), enabled);
87
+ },
88
+ deleteFriend: async (userId) => {
89
+ await this.client.deleteFriend(Number(userId));
90
+ },
91
+ });
92
+ control = Object.freeze({
93
+ recall: async (message) => {
94
+ if (!message.id || message.id.startsWith('outbound:'))
95
+ return;
96
+ await this.client.deleteMsg(message.id);
97
+ },
98
+ addReaction: async (message, emoji) => {
99
+ const target = resolveIcqqGroupReactionTarget(message);
100
+ if (!target)
101
+ return null;
102
+ try {
103
+ // Do not await protocol ACK — packet timeout must not stall the AI reply.
104
+ this.#ignoreReactionAck('add', this.client.pickGroup(target.groupId).setReaction(target.seq, emoji));
105
+ }
106
+ catch (error) {
107
+ this.#logger.debug(formatCompact({
108
+ op: 'reaction_add_failed',
109
+ error: error instanceof Error ? error.message : String(error),
110
+ }));
111
+ }
112
+ return emoji;
113
+ },
114
+ removeReaction: async (message, reactionId) => {
115
+ const target = resolveIcqqGroupReactionTarget(message);
116
+ if (!target)
117
+ return;
118
+ try {
119
+ this.#ignoreReactionAck('remove', this.client.pickGroup(target.groupId).delReaction(target.seq, reactionId));
120
+ }
121
+ catch (error) {
122
+ this.#logger.debug(formatCompact({
123
+ op: 'reaction_remove_failed',
124
+ error: error instanceof Error ? error.message : String(error),
125
+ }));
126
+ }
127
+ },
128
+ });
129
+ constructor(options) {
130
+ super();
131
+ const nativeConfig = resolveNativeClientConfig(options.config);
132
+ this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig);
133
+ this.#contentResolver = new IcqqContentResolver(this.client);
134
+ this.content = this.#contentResolver.port;
135
+ this.#logger = getAdapterLogger('icqq', options.config.id);
136
+ this.#options = options;
137
+ this.endpointName = options.config.id;
138
+ }
139
+ #ignoreReactionAck(action, task) {
140
+ void Promise.resolve(task).catch((error) => {
141
+ this.#logger.debug(formatCompact({
142
+ op: `reaction_${action}_failed`,
143
+ error: error instanceof Error ? error.message : String(error),
144
+ }));
145
+ });
146
+ }
147
+ async start(signal) {
148
+ if (this.#started)
149
+ return;
150
+ signal.throwIfAborted();
151
+ // Invalidate a guard left by a prior lifecycle epoch before admitting a
152
+ // new login attempt on this endpoint instance.
153
+ this.#retirementEpoch += 1;
154
+ this.#retirementArmed = false;
155
+ this.#retiring = false;
156
+ this.client.off('system.online');
157
+ this.#started = true;
158
+ let resolveOnline;
159
+ const onlineReady = new Promise((resolve) => {
160
+ resolveOnline = resolve;
161
+ });
162
+ const onAbort = () => resolveOnline();
163
+ this.#resolveStartOnline = resolveOnline;
164
+ signal.addEventListener('abort', onAbort, { once: true });
165
+ try {
166
+ this.#bindClientEvents();
167
+ await raceAbort(Promise.resolve(this.client.login(this.#options.config.password)), signal);
168
+ await onlineReady;
169
+ signal.throwIfAborted();
170
+ await this.#pullPendingSystemMessages(signal);
171
+ signal.throwIfAborted();
172
+ this.#logger.info(`connected (direct) | friends: ${this.client.fl.size} | groups: ${this.client.gl.size}`);
173
+ }
174
+ catch (error) {
175
+ await this.stop();
176
+ this.#logger.debug(`Failed to connect ICQQ client: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
177
+ throw error;
178
+ }
179
+ finally {
180
+ signal.removeEventListener('abort', onAbort);
181
+ if (this.#resolveStartOnline === resolveOnline)
182
+ this.#resolveStartOnline = undefined;
183
+ }
184
+ }
185
+ open() {
186
+ if (this.#retirementArmed) {
187
+ this.#retirementEpoch += 1;
188
+ this.#retirementArmed = false;
189
+ this.client.off('system.online');
190
+ this.#bindSystemOnlineEvent();
191
+ }
192
+ this.#retiring = false;
193
+ this.#open = true;
194
+ const held = this.#heldInbound.splice(0);
195
+ for (const msg of held) {
196
+ void this.admit(msg).catch((error) => {
197
+ this.#logInboundHandlerFailure('held', error);
198
+ });
199
+ }
200
+ }
201
+ async close() {
202
+ this.#assertExternalLifecycle('close');
203
+ this.#armRetirementFence();
204
+ await Promise.allSettled([...this.#inflightInbound]);
205
+ }
206
+ async stop() {
207
+ this.#assertExternalLifecycle('stop');
208
+ this.#armRetirementFence();
209
+ await Promise.allSettled([...this.#inflightInbound]);
210
+ this.#resolveStartOnline?.();
211
+ this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'endpoint_stopped');
212
+ this.#heldInbound.length = 0;
213
+ this.#disposeNativeEvents?.();
214
+ this.#disposeNativeEvents = undefined;
215
+ for (const event of BOUND_EVENTS) {
216
+ // The retirement guard owns this matcher until a late login handshake
217
+ // has finished. start()/open() replace it with the normal listener.
218
+ if (event !== 'system.online')
219
+ this.client.off(event);
220
+ }
221
+ // `Client.logout()` sends an account-level unregister packet before it
222
+ // closes this transport. During config HMR the replacement endpoint for
223
+ // the same UIN is already online, so unregistering the retiring generation
224
+ // also closes the replacement session. Endpoint ownership is transport
225
+ // scoped: retire only this Client's TCP connection.
226
+ try {
227
+ this.client.terminate();
228
+ }
229
+ catch { /* ignore */ }
230
+ this.#inboundDeduper.clear();
231
+ this.#sideEventDeduper.clear();
232
+ this.#guildCatalog.clear();
233
+ this.#started = false;
234
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
235
+ }
236
+ #armRetirementFence() {
237
+ this.#retiring = true;
238
+ this.#open = false;
239
+ if (this.#retirementArmed)
240
+ return;
241
+ this.#retirementArmed = true;
242
+ const retirementEpoch = ++this.#retirementEpoch;
243
+ // ICQQ owns reconnect through this per-Client timer. Clear it before any
244
+ // asynchronous drain so a queued callback cannot enter while close waits.
245
+ this.client.cancelReconnect();
246
+ // A login callback may already be inside the SDK handshake, which has no
247
+ // abort API. Replace the normal online listener with an epoch-owned guard
248
+ // before draining inbound work. Transport termination is account-local;
249
+ // logout would also unregister an HMR replacement using the same UIN.
250
+ this.client.off('system.online');
251
+ this.client.on('system.online', () => {
252
+ if (this.#retirementEpoch !== retirementEpoch || !this.#retiring)
253
+ return;
254
+ try {
255
+ this.client.terminate();
256
+ }
257
+ catch { /* already closed */ }
258
+ });
259
+ }
260
+ async send({ conversation, payload }) {
261
+ if (!this.#started)
262
+ throw new Error('icqq endpoint 未连接');
263
+ const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
264
+ const prepared = isIcqqSegmentPayload(payload)
265
+ ? prepareIcqqOutboundMedia(payload, mediaMode)
266
+ : { content: payload, dispose() { } };
267
+ try {
268
+ const message = formatOutboundBody(prepared.content);
269
+ const preview = formatIcqqSendablePreview(message);
270
+ const parsed = icqqOutboundTarget(conversation);
271
+ let result;
272
+ try {
273
+ result = await this.#sendNative(parsed, message);
274
+ }
275
+ catch (error) {
276
+ this.#logger.warn(formatCompact({
277
+ op: 'icqq_send_failed',
278
+ target: `${conversation.kind}:${conversation.id}`,
279
+ error: describeUnknownError(error),
280
+ preview: truncatePreview(preview, 120),
281
+ }));
282
+ throw error;
283
+ }
284
+ const messageId = String(result?.message_id ?? `sent_${Date.now()}`);
285
+ this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(preview, 80)}`);
286
+ return messageId;
287
+ }
288
+ finally {
289
+ prepared.dispose();
290
+ }
291
+ }
292
+ async #sendNative(target, message) {
293
+ if (isIcqqShareElement(message)) {
294
+ switch (target.kind) {
295
+ case 'private':
296
+ await this.client.pickFriend(target.userId).share(message);
297
+ return {};
298
+ case 'group':
299
+ await this.client.pickGroup(target.groupId).share(message);
300
+ return {};
301
+ case 'temp':
302
+ await this.client.pickMember(target.groupId, target.userId).share(message);
303
+ return {};
304
+ case 'channel': {
305
+ const channel = this.client.pickGuild(target.guildId).channels.get(target.channelId);
306
+ if (!channel) {
307
+ throw new TypeError(`ICQQ guild channel is unavailable: ${target.guildId}/${target.channelId}`);
308
+ }
309
+ await channel.share(message);
310
+ return {};
311
+ }
312
+ }
313
+ }
314
+ if (isIcqqFileElement(message)) {
315
+ switch (target.kind) {
316
+ case 'private': {
317
+ const id = await this.client.pickFriend(target.userId).sendFile(message.file, message.name);
318
+ return { message_id: id };
319
+ }
320
+ case 'group': {
321
+ const stat = await this.client.pickGroup(target.groupId).sendFile(message.file, '/', message.name);
322
+ return { message_id: stat.fid };
323
+ }
324
+ case 'temp':
325
+ case 'channel':
326
+ throw new TypeError(`ICQQ ${target.kind} conversation does not support file delivery`);
327
+ }
328
+ }
329
+ switch (target.kind) {
330
+ case 'private':
331
+ return this.client.sendPrivateMsg(target.userId, message);
332
+ case 'group':
333
+ return this.client.sendGroupMsg(target.groupId, message);
334
+ case 'temp':
335
+ return this.client.sendTempMsg(target.groupId, target.userId, message);
336
+ case 'channel':
337
+ return this.client.sendGuildMsg(target.guildId, target.channelId, message);
338
+ }
339
+ }
340
+ admit(msg) {
341
+ return this.#trackInbound(() => this.#deliverInbound(msg));
342
+ }
343
+ #trackInbound(run) {
344
+ const owner = Symbol('icqq-inbound');
345
+ let settle;
346
+ const settlement = new Promise((resolve) => { settle = resolve; });
347
+ this.#inflightInbound.add(settlement);
348
+ let operation;
349
+ try {
350
+ operation = this.#inboundOwner.run(owner, run);
351
+ }
352
+ catch (error) {
353
+ this.#inflightInbound.delete(settlement);
354
+ settle();
355
+ return Promise.reject(error);
356
+ }
357
+ return operation.finally(() => {
358
+ this.#inflightInbound.delete(settlement);
359
+ settle();
360
+ });
361
+ }
362
+ #assertExternalLifecycle(operation) {
363
+ if (!this.#inboundOwner.getStore())
364
+ return;
365
+ throw new Error(`ICQQ endpoint ${operation} cannot run inside its own inbound receive; invoke it from the owning lifecycle`);
366
+ }
367
+ async #deliverInbound(msg, admittedBeforeRetire = false) {
368
+ const conversation = msg.conversation;
369
+ if (!this.#open && !admittedBeforeRetire) {
370
+ if (this.#retiring)
371
+ return;
372
+ if (this.#heldInbound.length >= INBOUND_HOLD_LIMIT) {
373
+ const dropped = this.#heldInbound.shift();
374
+ this.#logger.warn(formatCompact({
375
+ op: 'icqq_inbound_hold_drop',
376
+ endpoint: this.endpointName,
377
+ id: dropped?.id,
378
+ target: dropped
379
+ ? `${dropped.conversation.kind}:${dropped.conversation.id}`
380
+ : undefined,
381
+ limit: INBOUND_HOLD_LIMIT,
382
+ }));
383
+ }
384
+ this.#heldInbound.push(msg);
385
+ this.#logger.info(`hold ${conversation.kind}:${conversation.id}`
386
+ + (msg.sender ? ` from ${msg.sender.name ?? msg.sender.id}` : '')
387
+ + ` until open | ${truncatePreview(msg.content, 80)}`);
388
+ return;
389
+ }
390
+ const mentioned = isIcqqBotMentioned({
391
+ uin: this.endpointName,
392
+ content: msg.segments,
393
+ rawMessage: msg.content,
394
+ });
395
+ this.#logger.info(`recv ${conversation.kind}:${conversation.id}`
396
+ + (msg.sender ? ` from ${msg.sender.name ?? msg.sender.id}` : '')
397
+ + (mentioned ? ' (mentioned)' : '')
398
+ + ` | ${truncatePreview(msg.content, 80)}`);
399
+ await this.emit('message.receive', {
400
+ conversation,
401
+ message: { conversation, id: msg.id },
402
+ content: msg.content,
403
+ ...(msg.segments ? { segments: msg.segments } : {}),
404
+ sender: msg.sender,
405
+ endpointId: this.endpointName,
406
+ ...(mentioned ? { mentioned: true } : {}),
407
+ ...(msg.replyTo ? { replyTo: msg.replyTo } : {}),
408
+ metadata: Object.freeze({
409
+ channelType: msg.channelType,
410
+ ...(msg.metadata ?? {}),
411
+ }),
412
+ }).catch((err) => {
413
+ this.#logger.warn(formatCompact({
414
+ op: 'recv',
415
+ endpoint: this.endpointName,
416
+ target: `${conversation.kind}:${conversation.id}`,
417
+ ok: false,
418
+ error: err instanceof Error ? err.message : String(err),
419
+ }));
420
+ });
421
+ }
422
+ #bindClientEvents() {
423
+ this.#bindNativeEvents();
424
+ const safe = (label, fn) => {
425
+ try {
426
+ fn();
427
+ }
428
+ catch (error) {
429
+ this.#logger.warn(formatCompact({
430
+ op: 'icqq_event_handler',
431
+ endpoint: this.endpointName,
432
+ event: label,
433
+ error: error instanceof Error ? error.message : String(error),
434
+ }));
435
+ }
436
+ };
437
+ this.client.on('message.private', (event) => {
438
+ this.#startMessageEvent('message.private', event);
439
+ });
440
+ this.client.on('message.group', (event) => {
441
+ this.#startMessageEvent('message.group', event);
442
+ });
443
+ this.client.on('message.guild', (event) => {
444
+ void this.#handleGuildEvent(event).catch((error) => {
445
+ this.#logInboundHandlerFailure('message.guild', error);
446
+ });
447
+ });
448
+ this.client.on('request.friend', (event) => safe('request.friend', () => {
449
+ this.#onRequestEvent(serializeIcqqEvent(event));
450
+ }));
451
+ this.client.on('request.group', (event) => safe('request.group', () => {
452
+ this.#onRequestEvent(serializeIcqqEvent(event));
453
+ }));
454
+ this.client.on('notice.friend', (event) => safe('notice.friend', () => {
455
+ this.#onNoticeEvent(serializeIcqqEvent(event));
456
+ }));
457
+ this.client.on('notice.group', (event) => safe('notice.group', () => {
458
+ this.#onNoticeEvent(serializeIcqqEvent(event));
459
+ }));
460
+ this.#bindSystemOnlineEvent();
461
+ this.client.on('system.offline', (event) => {
462
+ this.#logSystemEvent('offline', event);
463
+ });
464
+ this.client.on('system.offline.network', (event) => {
465
+ this.#logSystemEvent('offline.network', event);
466
+ });
467
+ this.client.on('system.offline.kickoff', (event) => {
468
+ this.#logSystemEvent('offline.kickoff', event);
469
+ });
470
+ this.client.on('system.login.qrcode', (event) => {
471
+ this.#logger.info('icqq 收到二维码登录事件,等待 LoginAssist / 终端确认后继续');
472
+ this.#handleLoginChallenge('qrcode', event);
473
+ });
474
+ this.client.on('system.login.device', (event) => {
475
+ this.#handleLoginChallenge('device', event);
476
+ });
477
+ this.client.on('system.login.slider', (event) => {
478
+ this.#handleLoginChallenge('slider', event);
479
+ });
480
+ this.client.on('system.login.error', (event) => {
481
+ this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'login_error');
482
+ this.#logSystemEvent('login.error', event);
483
+ });
484
+ this.client.on('system.login.auth', (event) => {
485
+ this.#handleLoginChallenge('auth', event);
486
+ });
487
+ }
488
+ #bindSystemOnlineEvent() {
489
+ this.client.on('system.online', (event) => {
490
+ if (this.#retiring)
491
+ return;
492
+ try {
493
+ this.#resolveStartOnline?.();
494
+ this.#options.loginAssist.cancelOwned(this.#loginAssistOwner, 'online');
495
+ this.#logSystemEvent('online', event);
496
+ }
497
+ catch (error) {
498
+ this.#logger.warn(formatCompact({
499
+ op: 'icqq_event_handler',
500
+ endpoint: this.endpointName,
501
+ event: 'system.online',
502
+ error: error instanceof Error ? error.message : String(error),
503
+ }));
504
+ }
505
+ });
506
+ }
507
+ /** Forward every public SDK event, including protocol extensions unknown to Zhin. */
508
+ #bindNativeEvents() {
509
+ this.#disposeNativeEvents?.();
510
+ let matchedName;
511
+ const registration = this.client.on((eventName) => {
512
+ matchedName = typeof eventName === 'string' && !eventName.startsWith('internal.')
513
+ ? eventName
514
+ : undefined;
515
+ return matchedName !== undefined;
516
+ }, (event) => {
517
+ const name = matchedName;
518
+ if (name)
519
+ this.#emitNativeEvent(name, event);
520
+ });
521
+ this.#disposeNativeEvents = () => registration();
522
+ }
523
+ #emitNativeEvent(name, event) {
524
+ void this.emitPlatform(name, event).catch((error) => {
525
+ this.#logger.warn(formatCompact({
526
+ op: 'icqq_platform_event_failed',
527
+ endpoint: this.endpointName,
528
+ event: name,
529
+ error: error instanceof Error ? error.message : String(error),
530
+ }));
531
+ });
532
+ }
533
+ #handleLoginChallenge(step, event) {
534
+ const payload = serializeIcqqEvent(event) ?? (event && typeof event === 'object'
535
+ ? event
536
+ : null);
537
+ this.#logSystemEvent(`login.${step}`, event);
538
+ void runIcqqLoginAssistStep({
539
+ assist: this.#options.loginAssist,
540
+ client: this.client,
541
+ adapter: 'icqq',
542
+ endpointKey: this.endpointName,
543
+ owner: this.#loginAssistOwner,
544
+ logger: this.#logger,
545
+ step,
546
+ event: payload ?? event,
547
+ });
548
+ }
549
+ #logSystemEvent(type, event) {
550
+ const payload = serializeIcqqEvent(event);
551
+ this.#logger.warn(formatCompact({
552
+ op: `system.${type}`,
553
+ endpoint: this.endpointName,
554
+ ...(payload ? { event: JSON.stringify(payload) } : {}),
555
+ }));
556
+ this.#dispatchIcqqSystemSideEvent(type, payload);
557
+ }
558
+ #dispatchIcqqSideEvent(payload) {
559
+ const postType = String(payload.post_type ?? '');
560
+ const isRequest = postType === 'request' || postType.startsWith('request.');
561
+ const flag = payload.flag != null ? String(payload.flag) : '';
562
+ void receiveOneBotLikeSideEvent((name, payload) => this.emit(name, payload), {
563
+ adapter: 'icqq',
564
+ endpointKey: this.endpointName,
565
+ platform: 'icqq',
566
+ raw: payload,
567
+ ...(isRequest ? {
568
+ approve: (id, remark) => this.#approveRequest(flag || id, remark),
569
+ reject: (id, reason) => this.#rejectRequest(flag || id, reason),
570
+ } : {}),
571
+ }).catch((err) => {
572
+ this.#logger.warn(formatCompact({
573
+ op: 'icqq_side_event_failed',
574
+ endpoint: this.endpointName,
575
+ post_type: postType || undefined,
576
+ error: err instanceof Error ? err.message : String(err),
577
+ }));
578
+ });
579
+ }
580
+ #dispatchIcqqSystemSideEvent(type, payload) {
581
+ const { scene_type, sub_type } = parseIcqqSystemScene(type);
582
+ void this.emit('system.receive', SystemEvent.from(payload ?? {}, {
583
+ $id: `system:${this.endpointName}:${type}:${Date.now()}`,
584
+ $adapter: 'icqq',
585
+ $endpoint: this.endpointName,
586
+ $type: 'system',
587
+ $scene_id: this.endpointName,
588
+ $scene_type: scene_type,
589
+ $sub_type: sub_type,
590
+ $timestamp: Date.now(),
591
+ })).catch((err) => {
592
+ this.#logger.warn(formatCompact({
593
+ op: 'icqq_system_side_event_failed',
594
+ endpoint: this.endpointName,
595
+ type,
596
+ error: err instanceof Error ? err.message : String(err),
597
+ }));
598
+ });
599
+ }
600
+ async #handleMessageEvent(eventName, event) {
601
+ if (this.#retiring)
602
+ return;
603
+ const admittedBeforeRetire = this.#open;
604
+ const payload = serializeIcqqEvent(event);
605
+ if (!payload || !isIcqqMessagePostType(payload)) {
606
+ this.#logger.debug(formatCompact({
607
+ op: 'icqq_inbound_skip',
608
+ reason: payload ? 'not_message_post' : 'serialize_failed',
609
+ endpoint: this.endpointName,
610
+ event: eventName,
611
+ }));
612
+ return;
613
+ }
614
+ const data = payload;
615
+ if (shouldSkipSelfInboundMessage(data)) {
616
+ this.#logger.debug(formatCompact({
617
+ op: 'icqq_inbound_skip',
618
+ reason: 'self',
619
+ endpoint: this.endpointName,
620
+ user_id: data.user_id,
621
+ self_id: data.self_id,
622
+ }));
623
+ return;
624
+ }
625
+ const contact = this.#resolveInboundContact(data);
626
+ if (contact) {
627
+ await resolveIcqqInboundMedia(data.message, {
628
+ getPttUrl: (element) => contact.getPttUrl(element),
629
+ getVideoUrl: (element) => contact.getVideoUrl(element),
630
+ });
631
+ if (!this.#started)
632
+ return;
633
+ }
634
+ const normalized = normalizeIcqqInboundMessage(data);
635
+ if (!normalized) {
636
+ this.#logger.debug(formatCompact({
637
+ op: 'icqq_inbound_skip',
638
+ reason: 'normalize_failed',
639
+ endpoint: this.endpointName,
640
+ event: eventName,
641
+ message_id: data.message_id,
642
+ }));
643
+ return;
644
+ }
645
+ if (!this.#inboundDeduper.shouldProcess(normalized.messageId)) {
646
+ this.#logger.debug(formatCompact({
647
+ op: 'icqq_inbound_skip',
648
+ reason: 'dedupe',
649
+ endpoint: this.endpointName,
650
+ id: normalized.messageId,
651
+ }));
652
+ return;
653
+ }
654
+ const conversation = icqqInboundConversation(String(this.#options.id), {
655
+ channelType: normalized.channelType,
656
+ channelId: normalized.channelId,
657
+ channelParentGroupId: normalized.channelParentGroupId,
658
+ });
659
+ const quoteId = resolveIcqqQuoteIdFromEvent(data);
660
+ const quoted = messageLookupFromIcqqSource(findIcqqNestedMessageSource(data));
661
+ if (quoteId && quoted) {
662
+ this.#contentResolver.remember(quoteId, Object.freeze({
663
+ ref: Object.freeze({ conversation, id: quoteId }),
664
+ ...(quoted.sender?.id ? { actor: Object.freeze({
665
+ id: quoted.sender.id,
666
+ ...(quoted.sender.name ? { displayName: quoted.sender.name } : {}),
667
+ }) } : {}),
668
+ segments: Object.freeze(toCanonicalSegments(Array.isArray(quoted.content)
669
+ ? quoted.content
670
+ : [{ type: 'text', data: { text: quoted.content } }])),
671
+ timestamp: quoted.time ? quoted.time * (quoted.time < 1e12 ? 1000 : 1) : Date.now(),
672
+ }));
673
+ }
674
+ await this.#deliverInbound({
675
+ id: normalized.messageId,
676
+ conversation,
677
+ content: formatInboundContent(normalized.rawMessage),
678
+ segments: normalized.content,
679
+ sender: {
680
+ id: normalized.userId,
681
+ name: normalized.nickname || undefined,
682
+ ...(normalized.senderRole ? { roles: [normalized.senderRole] } : {}),
683
+ },
684
+ channelType: normalized.channelType,
685
+ ...(quoteId ? { replyTo: { id: quoteId } } : {}),
686
+ metadata: {
687
+ nickname: normalized.nickname,
688
+ senderRole: normalized.senderRole,
689
+ },
690
+ }, admittedBeforeRetire);
691
+ }
692
+ #startMessageEvent(eventName, event) {
693
+ const operation = this.#trackInbound(() => this.#handleMessageEvent(eventName, event));
694
+ void operation.catch((error) => {
695
+ this.#logInboundHandlerFailure(eventName, error);
696
+ });
697
+ }
698
+ #resolveInboundContact(data) {
699
+ const userId = Number(data.user_id ?? data.from_id);
700
+ if (!Number.isFinite(userId) || userId <= 0)
701
+ return null;
702
+ if (data.message_type === 'group' || data.group_id != null) {
703
+ const groupId = Number(data.group_id);
704
+ return Number.isFinite(groupId) && groupId > 0 ? this.client.pickGroup(groupId) : null;
705
+ }
706
+ const sourceGroupId = Number(data.sender?.group_id);
707
+ if (Number.isFinite(sourceGroupId) && sourceGroupId > 0) {
708
+ return this.client.pickMember(sourceGroupId, userId);
709
+ }
710
+ return this.client.pickFriend(userId);
711
+ }
712
+ #logInboundHandlerFailure(event, error) {
713
+ this.#logger.warn(formatCompact({
714
+ op: 'icqq_inbound_handler_failed',
715
+ endpoint: this.endpointName,
716
+ event,
717
+ error: describeUnknownError(error),
718
+ }));
719
+ }
720
+ async #handleGuildEvent(event) {
721
+ const payload = serializeIcqqEvent(event);
722
+ if (!payload || typeof payload !== 'object')
723
+ return;
724
+ const normalized = normalizeIcqqGuildInboundMessage(payload);
725
+ if (!normalized) {
726
+ this.#logger.debug(formatCompact({
727
+ op: 'icqq_inbound_skip',
728
+ reason: 'guild_normalize_failed',
729
+ endpoint: this.endpointName,
730
+ }));
731
+ return;
732
+ }
733
+ this.#guildCatalog.upsertFromInbound(normalized.raw);
734
+ if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) {
735
+ this.#logger.debug(formatCompact({
736
+ op: 'icqq_inbound_skip',
737
+ reason: 'dedupe',
738
+ endpoint: this.endpointName,
739
+ id: `guild:${normalized.messageId}`,
740
+ }));
741
+ return;
742
+ }
743
+ await this.admit({
744
+ id: normalized.messageId,
745
+ conversation: icqqInboundConversation(String(this.#options.id), {
746
+ channelType: 'channel',
747
+ channelId: normalized.channelId,
748
+ guildId: normalized.guildId,
749
+ }),
750
+ content: formatInboundContent(normalized.rawMessage),
751
+ segments: normalized.content,
752
+ sender: { id: normalized.userId, name: normalized.nickname || undefined },
753
+ channelType: 'channel',
754
+ metadata: {
755
+ guildId: normalized.guildId,
756
+ nickname: normalized.nickname,
757
+ },
758
+ });
759
+ }
760
+ #onRequestEvent(payload) {
761
+ if (!payload || !isIcqqRequestPayload(payload))
762
+ return;
763
+ const flag = payload.flag != null ? String(payload.flag) : (payload.seq != null ? String(payload.seq) : '');
764
+ if (flag && !this.#sideEventDeduper.shouldProcess(`request:${flag}`))
765
+ return;
766
+ this.#dispatchIcqqSideEvent(payload);
767
+ }
768
+ #onNoticeEvent(payload) {
769
+ if (!payload || !isIcqqNoticePayload(payload))
770
+ return;
771
+ const key = [
772
+ payload.time,
773
+ payload.operator_id ?? payload.user_id,
774
+ payload.notice_type ?? payload.type,
775
+ payload.sub_type,
776
+ ].join('_');
777
+ if (!this.#sideEventDeduper.shouldProcess(`notice:${key}`))
778
+ return;
779
+ this.#dispatchIcqqSideEvent(payload);
780
+ }
781
+ async #listPendingRequests() {
782
+ const messages = await this.client.getSystemMsg();
783
+ const rows = [];
784
+ for (const raw of messages) {
785
+ const msg = raw;
786
+ const kind = msg.request_type === 'friend' ? 'friend' : 'group';
787
+ const platformRequestId = (msg.flag && String(msg.flag).trim())
788
+ || (msg.seq != null ? String(msg.seq) : '');
789
+ if (!platformRequestId || msg.user_id == null)
790
+ continue;
791
+ const createdAt = Number(msg.time);
792
+ rows.push(Object.freeze({
793
+ platform_request_id: platformRequestId,
794
+ type: kind,
795
+ scene_type: kind === 'group' ? 'group' : null,
796
+ scene_id: String(kind === 'group' ? (msg.group_id ?? msg.user_id) : msg.user_id),
797
+ sub_type: kind === 'group' ? (msg.type != null ? String(msg.type) : null) : null,
798
+ actor_id: String(msg.user_id),
799
+ actor_name: msg.nickname != null ? String(msg.nickname) : null,
800
+ comment: msg.comment != null ? String(msg.comment) : null,
801
+ created_at: Number.isFinite(createdAt) && createdAt > 0
802
+ ? (createdAt < 1e12 ? createdAt * 1000 : createdAt)
803
+ : Date.now(),
804
+ }));
805
+ }
806
+ return Object.freeze(rows);
807
+ }
808
+ async #pullPendingSystemMessages(signal) {
809
+ try {
810
+ const messages = await raceAbort(Promise.resolve(this.client.getSystemMsg()), signal);
811
+ for (const raw of messages) {
812
+ signal.throwIfAborted();
813
+ const msg = raw;
814
+ const kind = msg.request_type === 'friend' ? 'friend' : 'group';
815
+ const flag = msg.flag != null ? String(msg.flag) : (msg.seq != null ? String(msg.seq) : '');
816
+ if (!flag || !this.#sideEventDeduper.shouldProcess(`request:${flag}`))
817
+ continue;
818
+ this.#dispatchIcqqSideEvent({
819
+ post_type: 'request',
820
+ request_type: kind,
821
+ flag: msg.flag,
822
+ user_id: msg.user_id,
823
+ group_id: msg.group_id,
824
+ comment: msg.comment,
825
+ time: msg.time,
826
+ sub_type: msg.type,
827
+ });
828
+ }
829
+ }
830
+ catch (error) {
831
+ if (signal.aborted)
832
+ throw signal.reason ?? error;
833
+ this.#logger.debug(formatCompact({
834
+ op: 'side_event_pull_system_msg',
835
+ endpoint: this.endpointName,
836
+ error: error instanceof Error ? error.message : String(error),
837
+ }));
838
+ }
839
+ }
840
+ async #approveRequest(id, remark) {
841
+ const messages = await this.client.getSystemMsg();
842
+ const target = messages.find((m) => {
843
+ const msg = m;
844
+ return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
845
+ });
846
+ if (!target?.flag) {
847
+ throw new Error(`icqq 未找到待处理请求: ${id}`);
848
+ }
849
+ if (target.request_type === 'friend') {
850
+ await this.client.setFriendAddRequest(target.flag, true, remark);
851
+ }
852
+ else {
853
+ await this.client.setGroupAddRequest(target.flag, true);
854
+ }
855
+ }
856
+ async #rejectRequest(id, reason) {
857
+ const messages = await this.client.getSystemMsg();
858
+ const target = messages.find((m) => {
859
+ const msg = m;
860
+ return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
861
+ });
862
+ if (!target?.flag) {
863
+ throw new Error(`icqq 未找到待处理请求: ${id}`);
864
+ }
865
+ if (target.request_type === 'friend') {
866
+ await this.client.setFriendAddRequest(target.flag, false);
867
+ }
868
+ else {
869
+ await this.client.setGroupAddRequest(target.flag, false, reason);
870
+ }
871
+ }
872
+ }
873
+ function isIcqqShareElement(message) {
874
+ return !Array.isArray(message)
875
+ && typeof message === 'object'
876
+ && message !== null
877
+ && message.type === 'share';
878
+ }
879
+ function formatIcqqSendablePreview(message) {
880
+ if (typeof message === 'string')
881
+ return message;
882
+ try {
883
+ return JSON.stringify(message, (_key, value) => Buffer.isBuffer(value) ? '<buffer>' : value);
884
+ }
885
+ catch {
886
+ return '[ICQQ Sendable]';
887
+ }
888
+ }
889
+ function isIcqqSegmentPayload(payload) {
890
+ if (Array.isArray(payload))
891
+ return true;
892
+ return typeof payload === 'object'
893
+ && payload !== null
894
+ && typeof payload.type === 'string';
895
+ }
896
+ function isIcqqFileElement(message) {
897
+ return !Array.isArray(message)
898
+ && typeof message !== 'string'
899
+ && message.type === 'file';
900
+ }
901
+ async function raceAbort(operation, signal) {
902
+ signal.throwIfAborted();
903
+ let rejectAbort;
904
+ const aborted = new Promise((_resolve, reject) => { rejectAbort = reject; });
905
+ const onAbort = () => rejectAbort(signal.reason ?? new Error('ICQQ operation aborted'));
906
+ signal.addEventListener('abort', onAbort, { once: true });
907
+ try {
908
+ return await Promise.race([operation, aborted]);
909
+ }
910
+ finally {
911
+ signal.removeEventListener('abort', onAbort);
912
+ }
913
+ }
914
+ function describeUnknownError(error) {
915
+ if (error instanceof Error)
916
+ return error.stack ?? error.message;
917
+ if (error && typeof error === 'object') {
918
+ try {
919
+ return JSON.stringify(error);
920
+ }
921
+ catch {
922
+ return inspect(error, { depth: 4, breakLength: 120 });
923
+ }
924
+ }
925
+ return String(error);
926
+ }
927
+ function resolveIcqqGroupReactionTarget(message) {
928
+ if (message.conversation.kind !== 'group')
929
+ return null;
930
+ const groupId = Number(message.conversation.id);
931
+ if (!Number.isFinite(groupId) || groupId <= 0)
932
+ return null;
933
+ const raw = message.id?.trim();
934
+ if (!raw || raw.startsWith('outbound:'))
935
+ return null;
936
+ if (/^[1-9]\d*$/.test(raw))
937
+ return { groupId, seq: Number(raw) };
938
+ try {
939
+ const parsed = parseGroupMessageId(raw);
940
+ if (parsed.seq > 0)
941
+ return { groupId: parsed.group_id || groupId, seq: parsed.seq };
942
+ }
943
+ catch {
944
+ // not a cqhttp group message id
945
+ }
946
+ return null;
947
+ }
948
+ function resolveNativeClientConfig(config) {
949
+ return {
950
+ log_level: 'info',
951
+ platform: config.platform,
952
+ ...(config.ver ? { ver: config.ver } : {}),
953
+ ...(config.dataDir ? { data_dir: config.dataDir } : {}),
954
+ ...(config.signApiAddr ? { sign_api_addr: config.signApiAddr } : {}),
955
+ ...(config.ignoreSelf != null ? { ignore_self: config.ignoreSelf } : {}),
956
+ ...(config.resend != null ? { resend: config.resend } : {}),
957
+ ...(config.cacheGroupMember != null ? { cache_group_member: config.cacheGroupMember } : {}),
958
+ ...(config.autoServer != null ? { auto_server: config.autoServer } : {}),
959
+ ...(config.qqnt != null ? { QQNT: config.qqnt } : {}),
960
+ ...(config.ntLogin != null ? { NTLogin: config.ntLogin } : {}),
961
+ reconn_interval: config.autoReconnect ? 5 : 0,
962
+ };
963
+ }
964
+ function serializeIcqqEvent(event) {
965
+ if (!event || typeof event !== 'object')
966
+ return null;
967
+ const nativeToJSON = event.toJSON;
968
+ if (typeof nativeToJSON === 'function') {
969
+ try {
970
+ const json = nativeToJSON.call(event, [...ICQQ_SERIALIZE_SKIP]);
971
+ if (json && typeof json === 'object' && !Array.isArray(json)) {
972
+ return json;
973
+ }
974
+ }
975
+ catch {
976
+ /* JSON.stringify 也会无参调 toJSON,不能当回退 */
977
+ }
978
+ }
979
+ const out = {};
980
+ for (const [key, value] of Object.entries(event)) {
981
+ if (typeof value === 'function' || ICQQ_SERIALIZE_SKIP.includes(key))
982
+ continue;
983
+ out[key] = value;
984
+ }
985
+ return Object.keys(out).length > 0 ? out : null;
986
+ }
987
+ function parseIcqqSystemScene(type) {
988
+ if (type.startsWith('login.')) {
989
+ return { scene_type: 'login', sub_type: type.slice('login.'.length) || 'unknown' };
990
+ }
991
+ if (type.startsWith('offline.')) {
992
+ return { scene_type: 'offline', sub_type: type.slice('offline.'.length) || 'unknown' };
993
+ }
994
+ if (type === 'online') {
995
+ return { scene_type: 'online', sub_type: 'online' };
996
+ }
997
+ if (type === 'offline') {
998
+ return { scene_type: 'offline', sub_type: 'offline' };
999
+ }
1000
+ return { scene_type: 'system', sub_type: type };
1001
+ }
1002
+ const INBOUND_HOLD_LIMIT = 32;
1003
+ const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'];