@zhin.js/adapter-icqq 8.0.9 → 8.0.11

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 (88) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +36 -28
  3. package/adapters/icqq.js +6 -11
  4. package/adapters/icqq.ts +7 -16
  5. package/agent/PERMITS.md +2 -0
  6. package/lib/agent-prompt.js +17 -22
  7. package/lib/cq-message.d.ts +2 -2
  8. package/lib/cq-message.js +2 -2
  9. package/lib/endpoint.d.ts +6 -76
  10. package/lib/endpoint.js +348 -507
  11. package/lib/forward-msg.d.ts +7 -8
  12. package/lib/forward-msg.js +11 -24
  13. package/lib/icqq-agent-deps.d.ts +3 -15
  14. package/lib/icqq-agent-deps.js +0 -5
  15. package/lib/icqq-endpoint-commands.js +6 -15
  16. package/lib/icqq-guild.d.ts +15 -11
  17. package/lib/icqq-guild.js +9 -17
  18. package/lib/icqq-inbound.d.ts +20 -20
  19. package/lib/icqq-inbound.js +9 -9
  20. package/lib/icqq-inbox.d.ts +2 -10
  21. package/lib/icqq-runtime-state.js +1 -1
  22. package/lib/index.d.ts +2 -3
  23. package/lib/index.js +2 -3
  24. package/lib/outbound-media.d.ts +3 -8
  25. package/lib/outbound-media.js +3 -8
  26. package/lib/protocol.d.ts +38 -57
  27. package/lib/protocol.js +25 -22
  28. package/lib/types.d.ts +4 -40
  29. package/lib/types.js +0 -5
  30. package/package.json +22 -13
  31. package/plugin.js +8 -3
  32. package/schema.json +119 -61
  33. package/src/agent-prompt.ts +17 -22
  34. package/src/cq-message.ts +2 -2
  35. package/src/endpoint.ts +360 -602
  36. package/src/forward-msg.ts +12 -26
  37. package/src/icqq-agent-deps.ts +3 -17
  38. package/src/icqq-endpoint-commands.ts +6 -15
  39. package/src/icqq-guild.ts +16 -25
  40. package/src/icqq-inbound.ts +26 -26
  41. package/src/icqq-inbox.ts +2 -10
  42. package/src/icqq-runtime-state.ts +1 -1
  43. package/src/index.ts +2 -9
  44. package/src/outbound-media.ts +4 -9
  45. package/src/protocol.ts +61 -67
  46. package/src/types.ts +4 -41
  47. package/tools/announce.js +19 -0
  48. package/tools/announce.ts +18 -0
  49. package/tools/essence.js +24 -0
  50. package/{agent/tools → tools}/essence.ts +8 -8
  51. package/tools/friend_list.js +20 -0
  52. package/{agent/tools → tools}/friend_list.ts +7 -7
  53. package/tools/group_files.js +25 -0
  54. package/tools/group_files.ts +23 -0
  55. package/tools/group_invite.js +25 -0
  56. package/tools/group_invite.ts +22 -0
  57. package/tools/group_kick.js +28 -0
  58. package/tools/group_kick.ts +24 -0
  59. package/tools/group_list.js +21 -0
  60. package/{agent/tools → tools}/group_list.ts +7 -7
  61. package/tools/group_sign.js +21 -0
  62. package/tools/group_sign.ts +19 -0
  63. package/tools/list_muted.js +20 -0
  64. package/tools/list_muted.ts +19 -0
  65. package/tools/poke.js +19 -0
  66. package/tools/poke.ts +18 -0
  67. package/tools/send_user_like.js +28 -0
  68. package/tools/send_user_like.ts +25 -0
  69. package/tools/set_anonymous.js +23 -0
  70. package/tools/set_anonymous.ts +21 -0
  71. package/tools/set_mute.js +26 -0
  72. package/tools/set_mute.ts +23 -0
  73. package/tools/set_title.js +27 -0
  74. package/tools/set_title.ts +24 -0
  75. package/tools/user_info.js +17 -0
  76. package/tools/user_info.ts +16 -0
  77. package/agent/tools/announce.ts +0 -21
  78. package/agent/tools/group_files.ts +0 -27
  79. package/agent/tools/group_invite.ts +0 -20
  80. package/agent/tools/group_sign.ts +0 -19
  81. package/agent/tools/list_muted.ts +0 -22
  82. package/agent/tools/poke.ts +0 -20
  83. package/agent/tools/send_user_like.ts +0 -22
  84. package/agent/tools/set_anonymous.ts +0 -22
  85. package/agent/tools/set_title.ts +0 -25
  86. package/lib/ipc-client.d.ts +0 -73
  87. package/lib/ipc-client.js +0 -358
  88. package/src/ipc-client.ts +0 -417
package/lib/endpoint.js CHANGED
@@ -1,502 +1,236 @@
1
+ import { Client, parseGroupMessageId, } from '@icqqjs/icqq';
2
+ import { inspect } from 'node:util';
1
3
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
2
4
  import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
3
- import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, shouldSkipSelfInboundMessage, unwrapIcqqIpcEventPayload, } from './icqq-inbound.js';
5
+ import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
4
6
  import { buildIcqqInboxNoticeRow, buildIcqqInboxRequestRow, buildIcqqSystemRequestRow, isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
5
- import { IcqqGuildCatalog, isIcqqGuildIpcEvent, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
6
- import { IpcClient } from './ipc-client.js';
7
+ import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
7
8
  import { materializeOutboundBase64, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
8
- import { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
9
- export class IcqqIpcEndpoint {
9
+ import { formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
10
+ const BOUND_EVENTS = [
11
+ 'message.private',
12
+ 'message.group',
13
+ 'message.guild',
14
+ 'request.friend',
15
+ 'request.group',
16
+ 'notice.friend',
17
+ 'notice.group',
18
+ 'system.online',
19
+ 'system.offline',
20
+ 'system.offline.network',
21
+ 'system.offline.kickoff',
22
+ 'system.login.qrcode',
23
+ 'system.login.device',
24
+ 'system.login.slider',
25
+ 'system.login.error',
26
+ 'system.login.auth',
27
+ ];
28
+ export class IcqqEndpoint extends Client {
10
29
  #logger;
11
30
  #options;
12
- #createIpc;
13
- name;
14
- /** Populated after start(); agent tools read this. */
15
- ipc;
16
- friends = new Map();
17
- groups = new Map();
31
+ endpointName;
18
32
  #guildCatalog = new IcqqGuildCatalog();
33
+ #inboundDeduper = new InboundMessageDeduper();
34
+ #inboxDeduper = new InboundMessageDeduper();
35
+ #open = false;
36
+ #started = false;
37
+ #heldInbound = [];
38
+ #unregisterAgent;
19
39
  management = Object.freeze({
20
- listFriends: () => this.getFriendList(),
21
- listGroups: () => this.getGroupList(),
40
+ listFriends: async () => Array.from(this.fl.values()).map((f) => ({
41
+ user_id: f.user_id,
42
+ nickname: f.nickname,
43
+ remark: f.remark ?? '',
44
+ })),
45
+ listGroups: async () => Array.from(this.gl.values()).map((g) => ({
46
+ group_id: g.group_id,
47
+ name: g.group_name,
48
+ })),
22
49
  listChannels: async () => {
23
- await this.#guildCatalog.syncAll(this.ipc);
50
+ await this.#guildCatalog.syncAll(this);
24
51
  return this.#guildCatalog.getGuildChannelList();
25
52
  },
26
- listGroupMembers: (groupId) => this.getGroupMemberList(groupId),
27
- approveRequest: (requestId, remark) => this.approveRequest(requestId, remark),
28
- rejectRequest: (requestId, reason) => this.rejectRequest(requestId, reason),
29
- kickGroupMember: (groupId, userId) => this.removeMember(groupId, userId),
30
- muteGroupMember: (groupId, userId, duration) => this.muteMember(groupId, userId, duration),
31
- setGroupAdmin: (groupId, userId, enabled) => this.setModerator(groupId, userId, enabled),
32
- deleteFriend: (userId) => this.deleteFriend(userId),
53
+ listGroupMembers: async (groupId) => Array.from((await this.getGroupMemberList(Number(groupId))).values()),
54
+ approveRequest: (requestId, remark) => this.#approveRequest(requestId, remark),
55
+ rejectRequest: (requestId, reason) => this.#rejectRequest(requestId, reason),
56
+ kickGroupMember: async (groupId, userId) => {
57
+ await this.setGroupKick(Number(groupId), Number(userId));
58
+ },
59
+ muteGroupMember: async (groupId, userId, duration) => {
60
+ await this.setGroupBan(Number(groupId), Number(userId), duration);
61
+ },
62
+ setGroupAdmin: async (groupId, userId, enabled) => {
63
+ await this.setGroupAdmin(Number(groupId), Number(userId), enabled);
64
+ },
65
+ deleteFriend: async (userId) => {
66
+ await this.deleteFriend(Number(userId));
67
+ },
33
68
  });
34
69
  control = Object.freeze({
35
- recall: (message) => this.recallMessage(message.id),
36
- addReaction: (message, emoji) => this.addReaction(message.id, emoji),
37
- removeReaction: (message, reactionId) => this.removeReaction(message.id, reactionId),
70
+ recall: async (message) => {
71
+ if (!message.id || message.id.startsWith('outbound:'))
72
+ return;
73
+ await this.deleteMsg(message.id);
74
+ },
75
+ addReaction: async (message, emoji) => {
76
+ const target = resolveIcqqGroupReactionTarget(message);
77
+ if (!target)
78
+ return null;
79
+ try {
80
+ // Do not await protocol ACK — packet timeout must not stall the AI reply.
81
+ this.#ignoreReactionAck('add', this.pickGroup(target.groupId).setReaction(target.seq, emoji));
82
+ }
83
+ catch (error) {
84
+ this.#logger.debug(formatCompact({
85
+ op: 'reaction_add_failed',
86
+ error: error instanceof Error ? error.message : String(error),
87
+ }));
88
+ }
89
+ return emoji;
90
+ },
91
+ removeReaction: async (message, reactionId) => {
92
+ const target = resolveIcqqGroupReactionTarget(message);
93
+ if (!target)
94
+ return;
95
+ try {
96
+ this.#ignoreReactionAck('remove', this.pickGroup(target.groupId).delReaction(target.seq, reactionId));
97
+ }
98
+ catch (error) {
99
+ this.#logger.debug(formatCompact({
100
+ op: 'reaction_remove_failed',
101
+ error: error instanceof Error ? error.message : String(error),
102
+ }));
103
+ }
104
+ },
38
105
  });
39
- #open = false;
40
- #started = false;
41
- #subscriptions = [];
42
- #inboundDeduper = new InboundMessageDeduper();
43
- /** request/notice 去重(推送事件与 GET_SYSTEM_MSG 首拉可能重复同一 flag)。 */
44
- #inboxDeduper = new InboundMessageDeduper();
45
- #unregisterAgent;
46
- /** 用户主动 stop 时为 true,阻止自动重连 */
47
- #intentionalDisconnect = false;
48
- /** 是否已有重连循环在跑(避免多次 schedule 叠套) */
49
- #reconnectRunning = false;
50
106
  constructor(options) {
107
+ const nativeConfig = resolveNativeClientConfig(options.config);
108
+ super(Number(options.config.id), nativeConfig);
51
109
  this.#logger = getAdapterLogger('icqq', options.config.id);
52
110
  this.#options = options;
53
- this.name = options.config.id;
54
- this.#createIpc = options.createIpc ?? defaultCreateIpc;
111
+ this.endpointName = options.config.id;
55
112
  }
56
- async request(action, params) {
57
- if (!this.ipc) {
58
- throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发起请求: ${action}`);
59
- }
60
- return this.ipc.request(action, params);
113
+ #ignoreReactionAck(action, task) {
114
+ void Promise.resolve(task).catch((error) => {
115
+ this.#logger.debug(formatCompact({
116
+ op: `reaction_${action}_failed`,
117
+ error: error instanceof Error ? error.message : String(error),
118
+ }));
119
+ });
61
120
  }
62
121
  async start() {
63
122
  if (this.#started)
64
123
  return;
65
124
  this.#started = true;
66
- this.#intentionalDisconnect = false;
67
125
  try {
68
- await this.#bindIpcSession();
69
- this.#unregisterAgent = registerIcqqAgentEndpoint(this.name, this);
70
- this.#logger.info(`connected (${this.#options.config.rpc ? 'rpc' : 'ipc'}) | friends: ${this.friends.size} | groups: ${this.groups.size}`);
126
+ const onlineReady = new Promise((resolve) => {
127
+ this.on('system.online', () => resolve());
128
+ });
129
+ this.#bindClientEvents();
130
+ await this.login(this.#options.config.password);
131
+ await onlineReady;
132
+ await this.#pullPendingSystemMessages();
133
+ this.#unregisterAgent = registerIcqqAgentEndpoint(this.endpointName, this);
134
+ this.#logger.info(`connected (direct) | friends: ${this.fl.size} | groups: ${this.gl.size}`);
71
135
  }
72
136
  catch (error) {
73
137
  await this.stop();
74
- // Startup connect failures are surfaced once (with stack) by AdapterIndex;
75
- // keep this at debug to avoid a duplicate error-level log.
76
- this.#logger.debug(`Failed to connect ICQQ IPC: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
138
+ this.#logger.debug(`Failed to connect ICQQ client: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
77
139
  throw error;
78
140
  }
79
141
  }
80
- /** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
81
- async #bindIpcSession() {
82
- // 旧连接上的订阅句柄已失效,不再对死 socket 发 unsubscribe
83
- this.#subscriptions = [];
84
- this.ipc = await this.#createIpc(this.#options.config);
85
- if (this.#options.config.autoReconnect !== false) {
86
- this.ipc.setOnRemoteDisconnect(() => this.#scheduleReconnect());
87
- }
88
- await this.#refreshLists();
89
- const sub = this.ipc.subscribe(Actions.SUBSCRIBE, {}, (event) => {
90
- this.#handleEvent(event);
91
- });
92
- this.#subscriptions.push(sub);
93
- // daemon 推送 client.em 全部事件,request/notice 实时到达;
94
- // 启动时补一次 GET_SYSTEM_MSG,捞离线期间积存的待处理请求。
95
- void this.#pullPendingSystemMessages();
96
- }
97
- /** IPC/RPC 意外断开时调度重连(指数退避) */
98
- #scheduleReconnect() {
99
- if (this.#options.config.autoReconnect === false)
100
- return;
101
- if (this.#intentionalDisconnect)
102
- return;
103
- if (this.#reconnectRunning)
104
- return;
105
- this.#reconnectRunning = true;
106
- void this.#runReconnectLoop();
107
- }
108
- async #runReconnectLoop() {
109
- try {
110
- for (let attempt = 0; !this.#intentionalDisconnect; attempt++) {
111
- const base = Math.min(30_000, 500 * 2 ** Math.min(attempt, 6));
112
- const jitter = Math.floor(Math.random() * 400);
113
- const delayMs = base + jitter;
114
- // 首次断开 WARN,后续重试静默为 DEBUG,避免刷屏
115
- const disconnectLog = attempt === 0 ? this.#logger.warn.bind(this.#logger) : this.#logger.debug.bind(this.#logger);
116
- disconnectLog(formatCompact({
117
- op: 'disconnect',
118
- ok: false,
119
- delay_ms: delayMs,
120
- attempt: attempt + 1,
121
- }));
122
- await new Promise((r) => setTimeout(r, delayMs));
123
- if (this.#intentionalDisconnect)
124
- break;
125
- try {
126
- await this.#bindIpcSession();
127
- this.#logger.info(`reconnected after ${attempt + 1} attempt(s)`
128
- + ` | friends: ${this.friends.size} | groups: ${this.groups.size}`);
129
- break;
130
- }
131
- catch (error) {
132
- const retryLog = attempt === 0 ? this.#logger.warn.bind(this.#logger) : this.#logger.debug.bind(this.#logger);
133
- retryLog(formatCompact({
134
- op: 'reconnect',
135
- endpoint: this.name,
136
- ok: false,
137
- attempt: attempt + 1,
138
- error: error instanceof Error ? error.message : String(error),
139
- }));
140
- }
141
- }
142
- }
143
- finally {
144
- this.#reconnectRunning = false;
145
- }
146
- }
147
142
  open() {
148
143
  this.#open = true;
144
+ const held = this.#heldInbound.splice(0);
145
+ for (const msg of held)
146
+ this.admit(msg);
149
147
  }
150
148
  close() {
151
149
  this.#open = false;
152
150
  }
153
151
  async stop() {
154
- this.#intentionalDisconnect = true;
155
152
  this.#open = false;
156
- this.ipc?.setOnRemoteDisconnect(null);
157
- for (const sub of this.#subscriptions.splice(0)) {
158
- await sub.unsubscribe().catch(() => { });
159
- }
153
+ this.#heldInbound.length = 0;
160
154
  this.#unregisterAgent?.();
161
155
  this.#unregisterAgent = undefined;
162
- this.#inboundDeduper.clear();
163
- this.#inboxDeduper.clear();
164
- this.friends.clear();
165
- this.groups.clear();
166
- this.#guildCatalog.clear();
156
+ for (const event of BOUND_EVENTS)
157
+ this.off(event);
167
158
  try {
168
- this.ipc?.close();
159
+ await this.logout?.();
169
160
  }
170
- catch {
171
- /* ignore */
161
+ catch { /* ignore */ }
162
+ try {
163
+ this.terminate();
172
164
  }
173
- // 置空 ipc:start 前 / stop 后的 request() 走防御性报错而非 TypeError
174
- this.ipc = undefined;
165
+ catch { /* ignore */ }
166
+ this.#inboundDeduper.clear();
167
+ this.#inboxDeduper.clear();
168
+ this.#guildCatalog.clear();
175
169
  this.#started = false;
176
170
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
177
171
  }
178
172
  async send({ conversation, payload }) {
179
- if (!this.ipc) {
180
- throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发送消息`);
181
- }
173
+ if (!this.#started)
174
+ throw new Error('icqq endpoint 未连接');
182
175
  const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
183
176
  const content = Array.isArray(payload)
184
177
  ? materializeOutboundBase64(payload, mediaMode)
185
178
  : payload;
186
179
  const message = formatOutboundBody(content);
187
180
  const parsed = icqqOutboundTarget(conversation);
188
- let action;
189
- let params;
190
- switch (parsed.kind) {
191
- case 'private':
192
- action = Actions.SEND_PRIVATE_MSG;
193
- params = { user_id: parsed.userId, message };
194
- break;
195
- case 'group':
196
- action = Actions.SEND_GROUP_MSG;
197
- params = { group_id: parsed.groupId, message };
198
- break;
199
- case 'temp':
200
- action = Actions.SEND_TEMP_MSG;
201
- params = {
202
- group_id: parsed.groupId,
203
- user_id: parsed.userId,
204
- message,
205
- };
206
- break;
207
- case 'channel':
208
- action = Actions.GUILD_SEND_MSG;
209
- params = {
210
- guild_id: parsed.guildId,
211
- channel_id: parsed.channelId,
212
- message,
213
- };
214
- break;
215
- }
216
- const resp = await this.ipc.request(action, params);
217
- if (!resp.ok) {
218
- this.#logger.warn(`send failed ${conversation.kind}:${conversation.id} | ${resp.error} | ${truncatePreview(typeof message === 'string' ? message : String(message), 80)}`);
219
- throw new Error(`发送消息失败: ${resp.error}`);
220
- }
221
- const messageId = String(resp.data?.message_id ?? `sent_${Date.now()}`);
222
- this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(typeof message === 'string' ? message : String(message), 80)}`);
223
- return messageId;
224
- }
225
- // ── 消息回应(activity-feedback / typing reaction)────────────────────
226
- static EMOJI_MAP = {
227
- '⏳': '1468368274',
228
- '👍': '128077',
229
- '❤️': '10084',
230
- '😊': '128522',
231
- '🎉': '127881',
232
- '🔥': '128293',
233
- '✅': '9989',
234
- '❌': '10060',
235
- '⭐': '11088',
236
- '💯': '128175',
237
- };
238
- getEmojiId(emoji) {
239
- if (/^\d+$/u.test(emoji))
240
- return emoji;
241
- return IcqqIpcEndpoint.EMOJI_MAP[emoji] ?? String(emoji.codePointAt(0) || 0);
242
- }
243
- /** Activity-feedback / OutboundHost reaction surface. */
244
- async addReaction(messageId, emoji) {
181
+ let result;
245
182
  try {
246
- const emojiId = this.getEmojiId(emoji);
247
- const resp = await this.request(Actions.GROUP_SET_REACTION, {
248
- message_id: messageId,
249
- id: emojiId,
250
- });
251
- if (!resp.ok) {
252
- this.#logger.warn(formatCompact({
253
- op: 'add_reaction',
254
- endpoint: this.name,
255
- message_id: messageId,
256
- emoji,
257
- id: emojiId,
258
- ok: false,
259
- error: resp.error,
260
- }));
261
- return null;
262
- }
263
- this.#logger.debug(formatCompact({
264
- op: 'add_reaction',
265
- endpoint: this.name,
266
- message_id: messageId,
267
- emoji,
268
- id: emojiId,
269
- ok: true,
270
- }));
271
- return emojiId;
272
- }
273
- catch (error) {
274
- this.#logger.warn(formatCompact({
275
- op: 'add_reaction',
276
- endpoint: this.name,
277
- message_id: messageId,
278
- emoji,
279
- ok: false,
280
- error: error instanceof Error ? error.message : String(error),
281
- }));
282
- return null;
283
- }
284
- }
285
- async removeReaction(messageId, reactionId) {
286
- try {
287
- const parts = reactionId.split('_');
288
- const emojiId = parts.length >= 2 ? parts[1] : reactionId;
289
- const resp = await this.request(Actions.GROUP_DEL_REACTION, {
290
- message_id: messageId,
291
- id: emojiId,
292
- });
293
- if (!resp.ok) {
294
- this.#logger.warn(formatCompact({
295
- op: 'remove_reaction',
296
- endpoint: this.name,
297
- message_id: messageId,
298
- reaction_id: reactionId,
299
- id: emojiId,
300
- ok: false,
301
- error: resp.error,
302
- }));
303
- return;
304
- }
305
- this.#logger.debug(formatCompact({
306
- op: 'remove_reaction',
307
- endpoint: this.name,
308
- message_id: messageId,
309
- reaction_id: reactionId,
310
- id: emojiId,
311
- ok: true,
312
- }));
313
- }
314
- catch (error) {
315
- this.#logger.warn(formatCompact({
316
- op: 'remove_reaction',
317
- endpoint: this.name,
318
- message_id: messageId,
319
- reaction_id: reactionId,
320
- ok: false,
321
- error: error instanceof Error ? error.message : String(error),
322
- }));
323
- }
324
- }
325
- /** Activity-feedback autoRemove / Console recall. */
326
- async recallMessage(messageId) {
327
- if (!messageId || messageId.startsWith('outbound:'))
328
- return;
329
- try {
330
- const resp = await this.request(Actions.RECALL_MSG, { message_id: messageId });
331
- if (!resp.ok) {
332
- this.#logger.warn(formatCompact({
333
- op: 'recall_message',
334
- endpoint: this.name,
335
- message_id: messageId,
336
- ok: false,
337
- error: resp.error,
338
- }));
339
- return;
183
+ switch (parsed.kind) {
184
+ case 'private':
185
+ result = await this.sendPrivateMsg(parsed.userId, message);
186
+ break;
187
+ case 'group':
188
+ result = await this.sendGroupMsg(parsed.groupId, message);
189
+ break;
190
+ case 'temp':
191
+ result = await this.sendTempMsg(parsed.groupId, parsed.userId, message);
192
+ break;
193
+ case 'channel':
194
+ result = await this.sendGuildMsg(parsed.guildId, parsed.channelId, message);
195
+ break;
340
196
  }
341
- this.#logger.debug(formatCompact({
342
- op: 'recall_message',
343
- endpoint: this.name,
344
- message_id: messageId,
345
- ok: true,
346
- }));
347
197
  }
348
198
  catch (error) {
349
199
  this.#logger.warn(formatCompact({
350
- op: 'recall_message',
351
- endpoint: this.name,
352
- message_id: messageId,
353
- ok: false,
354
- error: error instanceof Error ? error.message : String(error),
200
+ op: 'icqq_send_failed',
201
+ target: `${conversation.kind}:${conversation.id}`,
202
+ error: describeUnknownError(error),
203
+ preview: truncatePreview(typeof message === 'string' ? message : String(message), 120),
355
204
  }));
205
+ throw error;
356
206
  }
207
+ const messageId = String(result?.message_id ?? `sent_${Date.now()}`);
208
+ this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(typeof message === 'string' ? message : String(message), 80)}`);
209
+ return messageId;
357
210
  }
358
- // ── 社交/群管(Console endpoint RPC 探测面)────────────────────────
359
- // console-rpc-extended.ts 按方法名探测这些接口;全部薄封装走 IPC daemon。
360
- /** EndpointManagement.listFriends —— 好友列表(LIST_FRIENDS,归一为 {user_id, nickname, remark})。 */
361
- async getFriendList() {
362
- const resp = await this.#mustRequest(Actions.LIST_FRIENDS, undefined, '获取好友列表');
363
- const list = Array.isArray(resp.data) ? resp.data : [];
364
- return list.map((f) => ({
365
- user_id: f.user_id,
366
- nickname: f.nickname,
367
- remark: f.remark ?? '',
368
- }));
369
- }
370
- /** EndpointManagement.listGroups —— 群列表(LIST_GROUPS,归一为 {group_id, name})。 */
371
- async getGroupList() {
372
- const resp = await this.#mustRequest(Actions.LIST_GROUPS, undefined, '获取群列表');
373
- const list = Array.isArray(resp.data) ? resp.data : [];
374
- return list.map((g) => ({
375
- group_id: g.group_id,
376
- name: g.group_name,
377
- }));
378
- }
379
- /** EndpointManagement.listGroupMembers —— 群成员列表(字段对齐 daemon 返回)。 */
380
- async getGroupMemberList(groupId) {
381
- const resp = await this.#mustRequest(Actions.LIST_GROUP_MEMBERS, { group_id: toNumericId(groupId, 'group_id') }, '获取群成员列表');
382
- return Array.isArray(resp.data) ? resp.data : [];
383
- }
384
- /** getGroupMemberList 别名(console 探测 listMembers / getMemberList)。 */
385
- listMembers(groupId) {
386
- return this.getGroupMemberList(groupId);
387
- }
388
- getMemberList(groupId) {
389
- return this.getGroupMemberList(groupId);
390
- }
391
- /**
392
- * EndpointManagement.resolveRequest —— id 为 inbox 行的 platform_request_id。
393
- * 先 GET_SYSTEM_MSG 按 flag(回退 seq)定位请求,再按类型路由 handle_friend/group_request。
394
- */
395
- async approveRequest(id, remark) {
396
- await this.#handleSystemRequest(id, true, { remark });
397
- }
398
- async rejectRequest(id, reason) {
399
- await this.#handleSystemRequest(id, false, { reason });
400
- }
401
- /** EndpointManagement.deleteFriend —— FRIEND_DELETE。 */
402
- async deleteFriend(userId) {
403
- await this.#mustRequest(Actions.FRIEND_DELETE, { user_id: toNumericId(userId, 'user_id') }, '删除好友');
404
- }
405
- /** deleteFriend 别名(console 探测 delete_friend)。 */
406
- delete_friend(userId) {
407
- return this.deleteFriend(userId);
408
- }
409
- /** EndpointManagement.kickGroupMember —— GROUP_KICK。 */
410
- async removeMember(groupId, userId) {
411
- await this.#mustRequest(Actions.GROUP_KICK, { group_id: toNumericId(groupId, 'group_id'), user_id: toNumericId(userId, 'user_id') }, '踢出群成员');
412
- }
413
- kickMember(groupId, userId) {
414
- return this.removeMember(groupId, userId);
415
- }
416
- setGroupKick(groupId, userId) {
417
- return this.removeMember(groupId, userId);
418
- }
419
- /** EndpointManagement.muteGroupMember —— GROUP_MUTE(duration 秒,默认 600)。 */
420
- async muteMember(groupId, userId, duration = 600) {
421
- await this.#mustRequest(Actions.GROUP_MUTE, {
422
- group_id: toNumericId(groupId, 'group_id'),
423
- user_id: toNumericId(userId, 'user_id'),
424
- duration,
425
- }, '禁言群成员');
426
- }
427
- banMember(groupId, userId, duration = 600) {
428
- return this.muteMember(groupId, userId, duration);
429
- }
430
- setGroupMute(groupId, userId, duration = 600) {
431
- return this.muteMember(groupId, userId, duration);
432
- }
433
- /** EndpointManagement.setGroupAdmin —— SET_GROUP_ADMIN(enable 默认 true)。 */
434
- async setModerator(groupId, userId, enable = true) {
435
- await this.#mustRequest(Actions.SET_GROUP_ADMIN, {
436
- group_id: toNumericId(groupId, 'group_id'),
437
- user_id: toNumericId(userId, 'user_id'),
438
- enable,
439
- }, '设置群管理员');
440
- }
441
- setAdmin(groupId, userId, enable = true) {
442
- return this.setModerator(groupId, userId, enable);
443
- }
444
- setGroupAdmin(groupId, userId, enable = true) {
445
- return this.setModerator(groupId, userId, enable);
446
- }
447
- /** IPC 请求并统一错误上下文:daemon 返回 ok=false 时抛出带操作名的错误。 */
448
- async #mustRequest(action, params, label) {
449
- const resp = await this.request(action, params);
450
- if (!resp.ok) {
451
- throw new Error(`icqq ${label}失败: ${resp.error ?? 'daemon 未返回错误详情'}`);
452
- }
453
- return resp;
454
- }
455
- /** 按 flag/seq 在 GET_SYSTEM_MSG 中定位请求,路由到好友/群请求处理 action。 */
456
- async #handleSystemRequest(id, approve, extra) {
457
- const resp = await this.#mustRequest(Actions.GET_SYSTEM_MSG, undefined, '获取待处理请求');
458
- const data = resp.data;
459
- const friendRequests = Array.isArray(data?.friendRequests)
460
- ? data.friendRequests
461
- : [];
462
- const groupRequests = Array.isArray(data?.groupRequests)
463
- ? data.groupRequests
464
- : [];
465
- const matches = (m) => m.flag === id || (m.seq != null && String(m.seq) === id);
466
- const friend = friendRequests.find(matches);
467
- const group = friend ? undefined : groupRequests.find(matches);
468
- const target = friend ?? group;
469
- if (!target?.flag) {
470
- throw new Error(`icqq 未找到待处理请求: ${id}(GET_SYSTEM_MSG 中无匹配 flag/seq)`);
471
- }
472
- const params = { flag: target.flag, approve };
473
- if (friend) {
474
- if (extra.remark)
475
- params.remark = extra.remark;
476
- await this.#mustRequest(Actions.HANDLE_FRIEND_REQUEST, params, '处理好友请求');
477
- }
478
- else {
479
- if (extra.reason)
480
- params.reason = extra.reason;
481
- await this.#mustRequest(Actions.HANDLE_GROUP_REQUEST, params, '处理群请求');
482
- }
483
- }
484
- /** Test / internal: admit when open. */
485
211
  admit(msg) {
486
212
  const conversation = msg.conversation;
487
213
  if (!this.#open) {
488
- this.#logger.debug(formatCompact({
489
- op: 'icqq_inbound_skip',
490
- reason: 'endpoint_closed',
491
- endpoint: this.name,
492
- id: msg.id,
493
- target: `${conversation.kind}:${conversation.id}`,
494
- }));
214
+ if (this.#heldInbound.length >= INBOUND_HOLD_LIMIT) {
215
+ const dropped = this.#heldInbound.shift();
216
+ this.#logger.warn(formatCompact({
217
+ op: 'icqq_inbound_hold_drop',
218
+ endpoint: this.endpointName,
219
+ id: dropped?.id,
220
+ target: dropped
221
+ ? `${dropped.conversation.kind}:${dropped.conversation.id}`
222
+ : undefined,
223
+ limit: INBOUND_HOLD_LIMIT,
224
+ }));
225
+ }
226
+ this.#heldInbound.push(msg);
227
+ this.#logger.info(`hold ${conversation.kind}:${conversation.id}`
228
+ + (msg.sender ? ` from ${msg.sender.name ?? msg.sender.id}` : '')
229
+ + ` until open | ${truncatePreview(msg.content, 80)}`);
495
230
  return;
496
231
  }
497
- // 新 Runtime Message.content 为纯文本:@ 本机(uin = name)经结构化段 / metadata 传递
498
232
  const mentioned = isIcqqBotMentioned({
499
- uin: this.name,
233
+ uin: this.endpointName,
500
234
  content: msg.segments,
501
235
  rawMessage: msg.content,
502
236
  });
@@ -512,7 +246,7 @@ export class IcqqIpcEndpoint {
512
246
  content: msg.content,
513
247
  ...(msg.segments ? { segments: msg.segments } : {}),
514
248
  sender: msg.sender,
515
- endpointId: this.name,
249
+ endpointId: this.endpointName,
516
250
  ...(mentioned ? { mentioned: true } : {}),
517
251
  ...(quoteId ? { replyTo: { id: String(quoteId) } } : {}),
518
252
  metadata: Object.freeze({
@@ -522,55 +256,70 @@ export class IcqqIpcEndpoint {
522
256
  }).catch((err) => {
523
257
  this.#logger.warn(formatCompact({
524
258
  op: 'recv',
525
- endpoint: this.name,
259
+ endpoint: this.endpointName,
526
260
  target: `${conversation.kind}:${conversation.id}`,
527
261
  ok: false,
528
262
  error: err instanceof Error ? err.message : String(err),
529
263
  }));
530
264
  });
531
265
  }
532
- async #refreshLists() {
533
- const [flResp, glResp] = await Promise.all([
534
- this.ipc.request(Actions.LIST_FRIENDS),
535
- this.ipc.request(Actions.LIST_GROUPS),
536
- ]);
537
- this.friends.clear();
538
- if (flResp.ok && Array.isArray(flResp.data)) {
539
- for (const f of flResp.data) {
540
- this.friends.set(f.user_id, f);
266
+ #bindClientEvents() {
267
+ const safe = (label, fn) => {
268
+ try {
269
+ fn();
541
270
  }
542
- }
543
- this.groups.clear();
544
- if (glResp.ok && Array.isArray(glResp.data)) {
545
- for (const g of glResp.data) {
546
- this.groups.set(g.group_id, g);
271
+ catch (error) {
272
+ this.#logger.warn(formatCompact({
273
+ op: 'icqq_event_handler',
274
+ endpoint: this.endpointName,
275
+ event: label,
276
+ error: error instanceof Error ? error.message : String(error),
277
+ }));
547
278
  }
548
- }
279
+ };
280
+ this.on('message.private', (event) => safe('message.private', () => this.#handleMessageEvent('message.private', event)));
281
+ this.on('message.group', (event) => safe('message.group', () => this.#handleMessageEvent('message.group', event)));
282
+ this.on('message.guild', (event) => safe('message.guild', () => this.#handleGuildEvent(event)));
283
+ this.on('request.friend', (event) => safe('request.friend', () => this.#recordInboxRequest(serializeIcqqEvent(event))));
284
+ this.on('request.group', (event) => safe('request.group', () => this.#recordInboxRequest(serializeIcqqEvent(event))));
285
+ this.on('notice.friend', (event) => safe('notice.friend', () => this.#recordInboxNotice(serializeIcqqEvent(event))));
286
+ this.on('notice.group', (event) => safe('notice.group', () => this.#recordInboxNotice(serializeIcqqEvent(event))));
287
+ this.on('system.offline', (event) => this.#logSystemEvent('offline', event));
288
+ this.on('system.offline.network', (event) => this.#logSystemEvent('offline.network', event));
289
+ this.on('system.offline.kickoff', (event) => this.#logSystemEvent('offline.kickoff', event));
290
+ this.on('system.login.qrcode', () => {
291
+ this.#logger.info('icqq 收到二维码登录事件,请按 icqq 默认流程扫码后继续登录');
292
+ });
293
+ this.on('system.login.device', (event) => this.#logSystemEvent('login.device', event));
294
+ this.on('system.login.slider', (event) => this.#logSystemEvent('login.slider', event));
295
+ this.on('system.login.error', (event) => this.#logSystemEvent('login.error', event));
296
+ this.on('system.login.auth', (event) => this.#logSystemEvent('login.auth', event));
297
+ }
298
+ #logSystemEvent(type, event) {
299
+ const payload = serializeIcqqEvent(event);
300
+ this.#logger.warn(formatCompact({
301
+ op: `system.${type}`,
302
+ endpoint: this.endpointName,
303
+ ...(payload ? { event: JSON.stringify(payload) } : {}),
304
+ }));
549
305
  }
550
- #handleEvent(event) {
551
- if (isIcqqGuildIpcEvent(event.event)) {
552
- this.#handleGuildEvent(event);
553
- return;
554
- }
555
- const payload = unwrapIcqqIpcEventPayload(event);
556
- if (!payload || typeof payload !== 'object')
557
- return;
558
- if (isIcqqRequestPayload(payload)) {
559
- this.#recordInboxRequest(payload);
560
- return;
561
- }
562
- if (isIcqqNoticePayload(payload)) {
563
- this.#recordInboxNotice(payload);
306
+ #handleMessageEvent(eventName, event) {
307
+ const payload = serializeIcqqEvent(event);
308
+ if (!payload || !isIcqqMessagePostType(payload)) {
309
+ this.#logger.debug(formatCompact({
310
+ op: 'icqq_inbound_skip',
311
+ reason: payload ? 'not_message_post' : 'serialize_failed',
312
+ endpoint: this.endpointName,
313
+ event: eventName,
314
+ }));
564
315
  return;
565
316
  }
566
- if (!isIcqqMessagePostType(payload))
567
- return;
568
317
  const data = payload;
569
318
  if (shouldSkipSelfInboundMessage(data)) {
570
319
  this.#logger.debug(formatCompact({
571
320
  op: 'icqq_inbound_skip',
572
321
  reason: 'self',
573
- endpoint: this.name,
322
+ endpoint: this.endpointName,
574
323
  user_id: data.user_id,
575
324
  self_id: data.self_id,
576
325
  }));
@@ -581,9 +330,9 @@ export class IcqqIpcEndpoint {
581
330
  this.#logger.debug(formatCompact({
582
331
  op: 'icqq_inbound_skip',
583
332
  reason: 'normalize_failed',
584
- endpoint: this.name,
333
+ endpoint: this.endpointName,
334
+ event: eventName,
585
335
  message_id: data.message_id,
586
- message_type: data.message_type ?? data.type,
587
336
  }));
588
337
  return;
589
338
  }
@@ -591,7 +340,7 @@ export class IcqqIpcEndpoint {
591
340
  this.#logger.debug(formatCompact({
592
341
  op: 'icqq_inbound_skip',
593
342
  reason: 'dedupe',
594
- endpoint: this.name,
343
+ endpoint: this.endpointName,
595
344
  id: normalized.messageId,
596
345
  }));
597
346
  return;
@@ -618,7 +367,7 @@ export class IcqqIpcEndpoint {
618
367
  });
619
368
  }
620
369
  #handleGuildEvent(event) {
621
- const payload = event.data;
370
+ const payload = serializeIcqqEvent(event);
622
371
  if (!payload || typeof payload !== 'object')
623
372
  return;
624
373
  const normalized = normalizeIcqqGuildInboundMessage(payload);
@@ -626,7 +375,7 @@ export class IcqqIpcEndpoint {
626
375
  this.#logger.debug(formatCompact({
627
376
  op: 'icqq_inbound_skip',
628
377
  reason: 'guild_normalize_failed',
629
- endpoint: this.name,
378
+ endpoint: this.endpointName,
630
379
  }));
631
380
  return;
632
381
  }
@@ -635,7 +384,7 @@ export class IcqqIpcEndpoint {
635
384
  this.#logger.debug(formatCompact({
636
385
  op: 'icqq_inbound_skip',
637
386
  reason: 'dedupe',
638
- endpoint: this.name,
387
+ endpoint: this.endpointName,
639
388
  id: `guild:${normalized.messageId}`,
640
389
  }));
641
390
  return;
@@ -657,12 +406,9 @@ export class IcqqIpcEndpoint {
657
406
  },
658
407
  });
659
408
  }
660
- /** 收件箱行公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
661
- #inboxBase() {
662
- const id = String(this.#options.id);
663
- return { adapter: id.split('\0').pop() ?? id, endpointKey: this.name };
664
- }
665
409
  #recordInboxRequest(payload) {
410
+ if (!payload || !isIcqqRequestPayload(payload))
411
+ return;
666
412
  const hooks = this.#options.inbox;
667
413
  if (!hooks)
668
414
  return;
@@ -675,6 +421,8 @@ export class IcqqIpcEndpoint {
675
421
  hooks.publish?.('endpoint:request', row);
676
422
  }
677
423
  #recordInboxNotice(payload) {
424
+ if (!payload || !isIcqqNoticePayload(payload))
425
+ return;
678
426
  const hooks = this.#options.inbox;
679
427
  if (!hooks)
680
428
  return;
@@ -686,24 +434,30 @@ export class IcqqIpcEndpoint {
686
434
  void hooks.recordNotice(row);
687
435
  hooks.publish?.('endpoint:notice', row);
688
436
  }
689
- /** 启动/重连后首拉 GET_SYSTEM_MSG:补录离线期间的好友/群待处理请求。 */
437
+ #inboxBase() {
438
+ const id = String(this.#options.id);
439
+ return { adapter: id.split('\0').pop() ?? id, endpointKey: this.endpointName };
440
+ }
690
441
  async #pullPendingSystemMessages() {
691
442
  const hooks = this.#options.inbox;
692
- if (!hooks || !this.ipc)
443
+ if (!hooks)
693
444
  return;
694
445
  try {
695
- const resp = await this.ipc.request(Actions.GET_SYSTEM_MSG);
696
- if (!resp.ok)
697
- return;
698
- const data = resp.data;
446
+ const messages = await this.getSystemMsg();
447
+ const friendRequests = [];
448
+ const groupRequests = [];
449
+ for (const msg of messages) {
450
+ if (msg.request_type === 'friend') {
451
+ friendRequests.push(msg);
452
+ }
453
+ else {
454
+ groupRequests.push(msg);
455
+ }
456
+ }
699
457
  const base = this.#inboxBase();
700
458
  const rows = [
701
- ...(Array.isArray(data?.friendRequests)
702
- ? data.friendRequests.map((m) => buildIcqqSystemRequestRow(m, 'friend', base))
703
- : []),
704
- ...(Array.isArray(data?.groupRequests)
705
- ? data.groupRequests.map((m) => buildIcqqSystemRequestRow(m, 'group', base))
706
- : []),
459
+ ...friendRequests.map((m) => buildIcqqSystemRequestRow(m, 'friend', base)),
460
+ ...groupRequests.map((m) => buildIcqqSystemRequestRow(m, 'group', base)),
707
461
  ];
708
462
  for (const row of rows) {
709
463
  if (!row)
@@ -717,17 +471,94 @@ export class IcqqIpcEndpoint {
717
471
  catch (error) {
718
472
  this.#logger.debug(formatCompact({
719
473
  op: 'inbox_pull_system_msg',
720
- endpoint: this.name,
474
+ endpoint: this.endpointName,
721
475
  error: error instanceof Error ? error.message : String(error),
722
476
  }));
723
477
  }
724
478
  }
479
+ async #approveRequest(id, remark) {
480
+ const messages = await this.getSystemMsg();
481
+ const target = messages.find((m) => {
482
+ const msg = m;
483
+ return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
484
+ });
485
+ if (!target?.flag) {
486
+ throw new Error(`icqq 未找到待处理请求: ${id}`);
487
+ }
488
+ if (target.request_type === 'friend') {
489
+ await this.setFriendAddRequest(target.flag, true, remark);
490
+ }
491
+ else {
492
+ await this.setGroupAddRequest(target.flag, true);
493
+ }
494
+ }
495
+ async #rejectRequest(id, reason) {
496
+ const messages = await this.getSystemMsg();
497
+ const target = messages.find((m) => {
498
+ const msg = m;
499
+ return msg.flag === id || (msg.seq != null && String(msg.seq) === id);
500
+ });
501
+ if (!target?.flag) {
502
+ throw new Error(`icqq 未找到待处理请求: ${id}`);
503
+ }
504
+ if (target.request_type === 'friend') {
505
+ await this.setFriendAddRequest(target.flag, false);
506
+ }
507
+ else {
508
+ await this.setGroupAddRequest(target.flag, false, reason);
509
+ }
510
+ }
511
+ }
512
+ function describeUnknownError(error) {
513
+ if (error instanceof Error)
514
+ return error.stack ?? error.message;
515
+ if (error && typeof error === 'object') {
516
+ try {
517
+ return JSON.stringify(error);
518
+ }
519
+ catch {
520
+ return inspect(error, { depth: 4, breakLength: 120 });
521
+ }
522
+ }
523
+ return String(error);
524
+ }
525
+ function resolveIcqqGroupReactionTarget(message) {
526
+ if (message.conversation.kind !== 'group')
527
+ return null;
528
+ const groupId = Number(message.conversation.id);
529
+ if (!Number.isFinite(groupId) || groupId <= 0)
530
+ return null;
531
+ const raw = message.id?.trim();
532
+ if (!raw || raw.startsWith('outbound:'))
533
+ return null;
534
+ if (/^[1-9]\d*$/.test(raw))
535
+ return { groupId, seq: Number(raw) };
536
+ try {
537
+ const parsed = parseGroupMessageId(raw);
538
+ if (parsed.seq > 0)
539
+ return { groupId: parsed.group_id || groupId, seq: parsed.seq };
540
+ }
541
+ catch {
542
+ // not a cqhttp group message id
543
+ }
544
+ return null;
545
+ }
546
+ function resolveNativeClientConfig(config) {
547
+ return {
548
+ log_level: 'info',
549
+ platform: config.platform,
550
+ ...(config.ver ? { ver: config.ver } : {}),
551
+ ...(config.dataDir ? { data_dir: config.dataDir } : {}),
552
+ ...(config.signApiAddr ? { sign_api_addr: config.signApiAddr } : {}),
553
+ ...(config.ignoreSelf != null ? { ignore_self: config.ignoreSelf } : {}),
554
+ ...(config.resend != null ? { resend: config.resend } : {}),
555
+ ...(config.cacheGroupMember != null ? { cache_group_member: config.cacheGroupMember } : {}),
556
+ ...(config.autoServer != null ? { auto_server: config.autoServer } : {}),
557
+ ...(config.qqnt != null ? { QQNT: config.qqnt } : {}),
558
+ ...(config.ntLogin != null ? { NTLogin: config.ntLogin } : {}),
559
+ reconn_interval: config.autoReconnect ? 5 : 0,
560
+ };
725
561
  }
726
- /**
727
- * 组装入站 metadata:在基础字段上补充 quote 链路信息。
728
- * - quote_id:被引用消息 id(resolveIcqqQuoteIdFromEvent,无则不写)
729
- * - quote_sender_id / quote_sender_name / quote_content:source 可用时的平铺摘要
730
- */
731
562
  function buildIcqqQuoteMetadata(data, base) {
732
563
  const metadata = { ...base };
733
564
  const quoteId = resolveIcqqQuoteIdFromEvent(data);
@@ -751,18 +582,28 @@ function buildIcqqQuoteMetadata(data, base) {
751
582
  metadata.quote_content = quoteText;
752
583
  return metadata;
753
584
  }
754
- /** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
755
- function toNumericId(value, label) {
756
- const n = Number(value);
757
- if (!Number.isFinite(n) || String(value).trim() === '') {
758
- throw new TypeError(`icqq ${label} 必须是数字: ${String(value)}`);
585
+ const INBOUND_HOLD_LIMIT = 32;
586
+ const ICQQ_SERIALIZE_SKIP = ['group', 'member', 'friend', 'discuss', 'client'];
587
+ function serializeIcqqEvent(event) {
588
+ if (!event || typeof event !== 'object')
589
+ return null;
590
+ const nativeToJSON = event.toJSON;
591
+ if (typeof nativeToJSON === 'function') {
592
+ try {
593
+ const json = nativeToJSON.call(event, [...ICQQ_SERIALIZE_SKIP]);
594
+ if (json && typeof json === 'object' && !Array.isArray(json)) {
595
+ return json;
596
+ }
597
+ }
598
+ catch {
599
+ /* JSON.stringify 也会无参调 toJSON,不能当回退 */
600
+ }
759
601
  }
760
- return n;
761
- }
762
- async function defaultCreateIpc(config) {
763
- const uin = Number(config.id);
764
- if (config.rpc) {
765
- return IpcClient.connectRpc(config.rpc);
602
+ const out = {};
603
+ for (const [key, value] of Object.entries(event)) {
604
+ if (typeof value === 'function' || ICQQ_SERIALIZE_SKIP.includes(key))
605
+ continue;
606
+ out[key] = value;
766
607
  }
767
- return IpcClient.connect(uin);
608
+ return Object.keys(out).length > 0 ? out : null;
768
609
  }