@zhin.js/adapter-icqq 6.0.2 → 7.0.0

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 (125) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +48 -435
  3. package/adapters/icqq.ts +92 -0
  4. package/agent/tools/announce.ts +2 -2
  5. package/agent/tools/essence.ts +2 -2
  6. package/agent/tools/friend_list.ts +2 -2
  7. package/agent/tools/group_files.ts +2 -2
  8. package/agent/tools/group_invite.ts +2 -2
  9. package/agent/tools/group_list.ts +2 -2
  10. package/agent/tools/group_sign.ts +2 -2
  11. package/agent/tools/list_muted.ts +2 -2
  12. package/agent/tools/poke.ts +2 -2
  13. package/agent/tools/send_user_like.ts +2 -2
  14. package/agent/tools/set_anonymous.ts +2 -2
  15. package/agent/tools/set_title.ts +2 -2
  16. package/lib/agent-prompt.d.ts +2 -0
  17. package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
  18. package/lib/cq-message.d.ts +9 -0
  19. package/lib/{src/cq-message.js → cq-message.js} +0 -1
  20. package/lib/endpoint.d.ts +101 -0
  21. package/lib/endpoint.js +715 -0
  22. package/lib/forward-msg.d.ts +26 -0
  23. package/lib/{src/forward-msg.js → forward-msg.js} +0 -1
  24. package/lib/icqq-agent-deps.d.ts +25 -0
  25. package/lib/icqq-agent-deps.js +32 -0
  26. package/lib/icqq-guild.d.ts +53 -0
  27. package/lib/{src/icqq-guild.js → icqq-guild.js} +4 -2
  28. package/lib/icqq-inbound.d.ts +139 -0
  29. package/lib/{src/icqq-inbound.js → icqq-inbound.js} +27 -3
  30. package/lib/icqq-inbox.d.ts +33 -0
  31. package/lib/icqq-inbox.js +129 -0
  32. package/lib/index.d.ts +6 -0
  33. package/lib/index.js +6 -0
  34. package/lib/ipc-client.d.ts +73 -0
  35. package/lib/{src/ipc-client.js → ipc-client.js} +0 -1
  36. package/lib/outbound-media.d.ts +13 -0
  37. package/lib/{src/outbound-media.js → outbound-media.js} +0 -1
  38. package/lib/protocol.d.ts +269 -0
  39. package/lib/{src/protocol.js → protocol.js} +124 -2
  40. package/lib/types.d.ts +87 -0
  41. package/lib/{src/types.js → types.js} +0 -1
  42. package/package.json +49 -45
  43. package/plugin.ts +32 -0
  44. package/schema.json +104 -0
  45. package/src/endpoint.ts +704 -795
  46. package/src/icqq-agent-deps.ts +48 -9
  47. package/src/icqq-guild.ts +10 -4
  48. package/src/icqq-inbound.ts +31 -2
  49. package/src/icqq-inbox.ts +155 -0
  50. package/src/index.ts +36 -69
  51. package/src/protocol.ts +191 -1
  52. package/src/types.ts +16 -0
  53. package/client/ICQQManagement.tsx +0 -147
  54. package/client/LoginAssistPanel.tsx +0 -300
  55. package/client/index.tsx +0 -11
  56. package/client/tsconfig.json +0 -7
  57. package/client/utils/api.ts +0 -30
  58. package/dist/index.js +0 -34
  59. package/lib/agent/tools/announce.js +0 -21
  60. package/lib/agent/tools/announce.js.map +0 -1
  61. package/lib/agent/tools/essence.js +0 -22
  62. package/lib/agent/tools/essence.js.map +0 -1
  63. package/lib/agent/tools/friend_list.js +0 -19
  64. package/lib/agent/tools/friend_list.js.map +0 -1
  65. package/lib/agent/tools/group_files.js +0 -29
  66. package/lib/agent/tools/group_files.js.map +0 -1
  67. package/lib/agent/tools/group_invite.js +0 -20
  68. package/lib/agent/tools/group_invite.js.map +0 -1
  69. package/lib/agent/tools/group_list.js +0 -20
  70. package/lib/agent/tools/group_list.js.map +0 -1
  71. package/lib/agent/tools/group_sign.js +0 -19
  72. package/lib/agent/tools/group_sign.js.map +0 -1
  73. package/lib/agent/tools/list_muted.js +0 -23
  74. package/lib/agent/tools/list_muted.js.map +0 -1
  75. package/lib/agent/tools/poke.js +0 -20
  76. package/lib/agent/tools/poke.js.map +0 -1
  77. package/lib/agent/tools/send_user_like.js +0 -22
  78. package/lib/agent/tools/send_user_like.js.map +0 -1
  79. package/lib/agent/tools/set_anonymous.js +0 -22
  80. package/lib/agent/tools/set_anonymous.js.map +0 -1
  81. package/lib/agent/tools/set_title.js +0 -25
  82. package/lib/agent/tools/set_title.js.map +0 -1
  83. package/lib/src/adapter.js +0 -128
  84. package/lib/src/adapter.js.map +0 -1
  85. package/lib/src/agent-prompt.js.map +0 -1
  86. package/lib/src/cq-message.js.map +0 -1
  87. package/lib/src/endpoint.js +0 -755
  88. package/lib/src/endpoint.js.map +0 -1
  89. package/lib/src/forward-msg.js.map +0 -1
  90. package/lib/src/get-msg.js +0 -46
  91. package/lib/src/get-msg.js.map +0 -1
  92. package/lib/src/icqq-agent-deps.js +0 -10
  93. package/lib/src/icqq-agent-deps.js.map +0 -1
  94. package/lib/src/icqq-guild.js.map +0 -1
  95. package/lib/src/icqq-inbound.js.map +0 -1
  96. package/lib/src/icqq-login-assist-bridge.js +0 -148
  97. package/lib/src/icqq-login-assist-bridge.js.map +0 -1
  98. package/lib/src/icqq-side-events.js +0 -130
  99. package/lib/src/icqq-side-events.js.map +0 -1
  100. package/lib/src/index.js +0 -50
  101. package/lib/src/index.js.map +0 -1
  102. package/lib/src/ipc-client.js.map +0 -1
  103. package/lib/src/login-assist-routes.js +0 -29
  104. package/lib/src/login-assist-routes.js.map +0 -1
  105. package/lib/src/login-ipc-contract.js +0 -35
  106. package/lib/src/login-ipc-contract.js.map +0 -1
  107. package/lib/src/outbound-media.js.map +0 -1
  108. package/lib/src/protocol.js.map +0 -1
  109. package/lib/src/routes.js +0 -67
  110. package/lib/src/routes.js.map +0 -1
  111. package/lib/src/segment-mapper.js +0 -2
  112. package/lib/src/segment-mapper.js.map +0 -1
  113. package/lib/src/tools/index.js +0 -23
  114. package/lib/src/tools/index.js.map +0 -1
  115. package/lib/src/types.js.map +0 -1
  116. package/plugin.yml +0 -3
  117. package/src/adapter.ts +0 -151
  118. package/src/get-msg.ts +0 -56
  119. package/src/icqq-login-assist-bridge.ts +0 -196
  120. package/src/icqq-side-events.ts +0 -143
  121. package/src/login-assist-routes.ts +0 -39
  122. package/src/login-ipc-contract.ts +0 -52
  123. package/src/routes.ts +0 -83
  124. package/src/segment-mapper.ts +0 -1
  125. package/src/tools/index.ts +0 -31
@@ -0,0 +1,715 @@
1
+ import { formatCompact, getLogger } from '@zhin.js/logger';
2
+ import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
3
+ import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, shouldSkipSelfInboundMessage, unwrapIcqqIpcEventPayload, } from './icqq-inbound.js';
4
+ 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 { materializeOutboundBase64, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
8
+ import { Actions, formatInboundContent, formatInboundTarget, formatOutboundBody, parseSendTarget, } from './protocol.js';
9
+ const logger = getLogger('icqq');
10
+ export class IcqqIpcEndpoint {
11
+ #options;
12
+ #createIpc;
13
+ name;
14
+ /** Populated after start(); agent tools read this. */
15
+ ipc;
16
+ friends = new Map();
17
+ groups = new Map();
18
+ #guildCatalog = new IcqqGuildCatalog();
19
+ management = Object.freeze({
20
+ listFriends: () => this.getFriendList(),
21
+ listGroups: () => this.getGroupList(),
22
+ listChannels: async () => {
23
+ await this.#guildCatalog.syncAll(this.ipc);
24
+ return this.#guildCatalog.getGuildChannelList();
25
+ },
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),
33
+ });
34
+ #open = false;
35
+ #started = false;
36
+ #subscriptions = [];
37
+ #inboundDeduper = new InboundMessageDeduper();
38
+ /** request/notice 去重(推送事件与 GET_SYSTEM_MSG 首拉可能重复同一 flag)。 */
39
+ #inboxDeduper = new InboundMessageDeduper();
40
+ #unregisterAgent;
41
+ /** 用户主动 stop 时为 true,阻止自动重连 */
42
+ #intentionalDisconnect = false;
43
+ /** 是否已有重连循环在跑(避免多次 schedule 叠套) */
44
+ #reconnectRunning = false;
45
+ constructor(options) {
46
+ this.#options = options;
47
+ this.name = options.config.name;
48
+ this.#createIpc = options.createIpc ?? defaultCreateIpc;
49
+ }
50
+ async request(action, params) {
51
+ if (!this.ipc) {
52
+ throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发起请求: ${action}`);
53
+ }
54
+ return this.ipc.request(action, params);
55
+ }
56
+ async start() {
57
+ if (this.#started)
58
+ return;
59
+ this.#started = true;
60
+ this.#intentionalDisconnect = false;
61
+ try {
62
+ await this.#bindIpcSession();
63
+ this.#unregisterAgent = registerIcqqAgentEndpoint(this.name, this);
64
+ logger.info(formatCompact({
65
+ op: 'connect',
66
+ endpoint: this.name,
67
+ mode: this.#options.config.rpc ? 'rpc' : 'ipc',
68
+ friends: this.friends.size,
69
+ groups: this.groups.size,
70
+ }));
71
+ }
72
+ catch (error) {
73
+ 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
+ logger.debug(`Failed to connect ICQQ IPC: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
77
+ throw error;
78
+ }
79
+ }
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 ? logger.warn.bind(logger) : logger.debug.bind(logger);
116
+ disconnectLog(formatCompact({
117
+ op: 'disconnect',
118
+ endpoint: this.name,
119
+ ok: false,
120
+ delay_ms: delayMs,
121
+ attempt: attempt + 1,
122
+ }));
123
+ await new Promise((r) => setTimeout(r, delayMs));
124
+ if (this.#intentionalDisconnect)
125
+ break;
126
+ try {
127
+ await this.#bindIpcSession();
128
+ logger.info(formatCompact({
129
+ op: 'reconnect',
130
+ endpoint: this.name,
131
+ ok: true,
132
+ attempts: attempt + 1,
133
+ friends: this.friends.size,
134
+ groups: this.groups.size,
135
+ }));
136
+ break;
137
+ }
138
+ catch (error) {
139
+ const retryLog = attempt === 0 ? logger.warn.bind(logger) : logger.debug.bind(logger);
140
+ retryLog(formatCompact({
141
+ op: 'reconnect',
142
+ endpoint: this.name,
143
+ ok: false,
144
+ attempt: attempt + 1,
145
+ error: error instanceof Error ? error.message : String(error),
146
+ }));
147
+ }
148
+ }
149
+ }
150
+ finally {
151
+ this.#reconnectRunning = false;
152
+ }
153
+ }
154
+ open() {
155
+ this.#open = true;
156
+ }
157
+ close() {
158
+ this.#open = false;
159
+ }
160
+ async stop() {
161
+ this.#intentionalDisconnect = true;
162
+ this.#open = false;
163
+ this.ipc?.setOnRemoteDisconnect(null);
164
+ for (const sub of this.#subscriptions.splice(0)) {
165
+ await sub.unsubscribe().catch(() => { });
166
+ }
167
+ this.#unregisterAgent?.();
168
+ this.#unregisterAgent = undefined;
169
+ this.#inboundDeduper.clear();
170
+ this.#inboxDeduper.clear();
171
+ this.friends.clear();
172
+ this.groups.clear();
173
+ this.#guildCatalog.clear();
174
+ try {
175
+ this.ipc?.close();
176
+ }
177
+ catch {
178
+ /* ignore */
179
+ }
180
+ // 置空 ipc:start 前 / stop 后的 request() 走防御性报错而非 TypeError
181
+ this.ipc = undefined;
182
+ this.#started = false;
183
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
184
+ }
185
+ async send({ target, payload }) {
186
+ if (!this.ipc) {
187
+ throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发送消息`);
188
+ }
189
+ const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
190
+ const content = Array.isArray(payload)
191
+ ? materializeOutboundBase64(payload, mediaMode)
192
+ : payload;
193
+ const message = formatOutboundBody(content);
194
+ const parsed = parseSendTarget(target);
195
+ let action;
196
+ let params;
197
+ switch (parsed.kind) {
198
+ case 'private':
199
+ action = Actions.SEND_PRIVATE_MSG;
200
+ params = { user_id: parsed.userId, message };
201
+ break;
202
+ case 'group':
203
+ action = Actions.SEND_GROUP_MSG;
204
+ params = { group_id: parsed.groupId, message };
205
+ break;
206
+ case 'temp':
207
+ action = Actions.SEND_TEMP_MSG;
208
+ params = {
209
+ group_id: parsed.groupId,
210
+ user_id: parsed.userId,
211
+ message,
212
+ };
213
+ break;
214
+ case 'channel':
215
+ action = Actions.GUILD_SEND_MSG;
216
+ params = {
217
+ guild_id: parsed.guildId,
218
+ channel_id: parsed.channelId,
219
+ message,
220
+ };
221
+ break;
222
+ }
223
+ const resp = await this.ipc.request(action, params);
224
+ if (!resp.ok) {
225
+ throw new Error(`发送消息失败: ${resp.error}`);
226
+ }
227
+ const messageId = String(resp.data?.message_id ?? `sent_${Date.now()}`);
228
+ logger.debug(formatCompact({
229
+ op: 'icqq_send',
230
+ endpoint: this.name,
231
+ target,
232
+ messageId,
233
+ }));
234
+ return messageId;
235
+ }
236
+ // ── 消息回应(activity-feedback / typing reaction)────────────────────
237
+ static EMOJI_MAP = {
238
+ '⏳': '1468368274',
239
+ '👍': '128077',
240
+ '❤️': '10084',
241
+ '😊': '128522',
242
+ '🎉': '127881',
243
+ '🔥': '128293',
244
+ '✅': '9989',
245
+ '❌': '10060',
246
+ '⭐': '11088',
247
+ '💯': '128175',
248
+ };
249
+ getEmojiId(emoji) {
250
+ if (/^\d+$/u.test(emoji))
251
+ return emoji;
252
+ return IcqqIpcEndpoint.EMOJI_MAP[emoji] ?? String(emoji.codePointAt(0) || 0);
253
+ }
254
+ /** Activity-feedback / OutboundHost reaction surface. */
255
+ async addReaction(messageId, emoji) {
256
+ try {
257
+ const emojiId = this.getEmojiId(emoji);
258
+ const resp = await this.request(Actions.GROUP_SET_REACTION, {
259
+ message_id: messageId,
260
+ id: emojiId,
261
+ });
262
+ if (!resp.ok) {
263
+ logger.warn(formatCompact({
264
+ op: 'add_reaction',
265
+ endpoint: this.name,
266
+ message_id: messageId,
267
+ emoji,
268
+ id: emojiId,
269
+ ok: false,
270
+ error: resp.error,
271
+ }));
272
+ return null;
273
+ }
274
+ logger.debug(formatCompact({
275
+ op: 'add_reaction',
276
+ endpoint: this.name,
277
+ message_id: messageId,
278
+ emoji,
279
+ id: emojiId,
280
+ ok: true,
281
+ }));
282
+ return emojiId;
283
+ }
284
+ catch (error) {
285
+ logger.warn(formatCompact({
286
+ op: 'add_reaction',
287
+ endpoint: this.name,
288
+ message_id: messageId,
289
+ emoji,
290
+ ok: false,
291
+ error: error instanceof Error ? error.message : String(error),
292
+ }));
293
+ return null;
294
+ }
295
+ }
296
+ async removeReaction(messageId, reactionId) {
297
+ try {
298
+ const parts = reactionId.split('_');
299
+ const emojiId = parts.length >= 2 ? parts[1] : reactionId;
300
+ const resp = await this.request(Actions.GROUP_DEL_REACTION, {
301
+ message_id: messageId,
302
+ id: emojiId,
303
+ });
304
+ if (!resp.ok) {
305
+ logger.warn(formatCompact({
306
+ op: 'remove_reaction',
307
+ endpoint: this.name,
308
+ message_id: messageId,
309
+ reaction_id: reactionId,
310
+ id: emojiId,
311
+ ok: false,
312
+ error: resp.error,
313
+ }));
314
+ return;
315
+ }
316
+ logger.debug(formatCompact({
317
+ op: 'remove_reaction',
318
+ endpoint: this.name,
319
+ message_id: messageId,
320
+ reaction_id: reactionId,
321
+ id: emojiId,
322
+ ok: true,
323
+ }));
324
+ }
325
+ catch (error) {
326
+ logger.warn(formatCompact({
327
+ op: 'remove_reaction',
328
+ endpoint: this.name,
329
+ message_id: messageId,
330
+ reaction_id: reactionId,
331
+ ok: false,
332
+ error: error instanceof Error ? error.message : String(error),
333
+ }));
334
+ }
335
+ }
336
+ /** Activity-feedback autoRemove / Console recall. */
337
+ async recallMessage(messageId) {
338
+ if (!messageId || messageId.startsWith('outbound:'))
339
+ return;
340
+ try {
341
+ const resp = await this.request(Actions.RECALL_MSG, { message_id: messageId });
342
+ if (!resp.ok) {
343
+ logger.warn(formatCompact({
344
+ op: 'recall_message',
345
+ endpoint: this.name,
346
+ message_id: messageId,
347
+ ok: false,
348
+ error: resp.error,
349
+ }));
350
+ return;
351
+ }
352
+ logger.debug(formatCompact({
353
+ op: 'recall_message',
354
+ endpoint: this.name,
355
+ message_id: messageId,
356
+ ok: true,
357
+ }));
358
+ }
359
+ catch (error) {
360
+ logger.warn(formatCompact({
361
+ op: 'recall_message',
362
+ endpoint: this.name,
363
+ message_id: messageId,
364
+ ok: false,
365
+ error: error instanceof Error ? error.message : String(error),
366
+ }));
367
+ }
368
+ }
369
+ // ── 社交/群管(Console endpoint RPC 探测面)────────────────────────
370
+ // console-rpc-extended.ts 按方法名探测这些接口;全部薄封装走 IPC daemon。
371
+ /** EndpointManagement.listFriends —— 好友列表(LIST_FRIENDS,归一为 {user_id, nickname, remark})。 */
372
+ async getFriendList() {
373
+ const resp = await this.#mustRequest(Actions.LIST_FRIENDS, undefined, '获取好友列表');
374
+ const list = Array.isArray(resp.data) ? resp.data : [];
375
+ return list.map((f) => ({
376
+ user_id: f.user_id,
377
+ nickname: f.nickname,
378
+ remark: f.remark ?? '',
379
+ }));
380
+ }
381
+ /** EndpointManagement.listGroups —— 群列表(LIST_GROUPS,归一为 {group_id, name})。 */
382
+ async getGroupList() {
383
+ const resp = await this.#mustRequest(Actions.LIST_GROUPS, undefined, '获取群列表');
384
+ const list = Array.isArray(resp.data) ? resp.data : [];
385
+ return list.map((g) => ({
386
+ group_id: g.group_id,
387
+ name: g.group_name,
388
+ }));
389
+ }
390
+ /** EndpointManagement.listGroupMembers —— 群成员列表(字段对齐 daemon 返回)。 */
391
+ async getGroupMemberList(groupId) {
392
+ const resp = await this.#mustRequest(Actions.LIST_GROUP_MEMBERS, { group_id: toNumericId(groupId, 'group_id') }, '获取群成员列表');
393
+ return Array.isArray(resp.data) ? resp.data : [];
394
+ }
395
+ /** getGroupMemberList 别名(console 探测 listMembers / getMemberList)。 */
396
+ listMembers(groupId) {
397
+ return this.getGroupMemberList(groupId);
398
+ }
399
+ getMemberList(groupId) {
400
+ return this.getGroupMemberList(groupId);
401
+ }
402
+ /**
403
+ * EndpointManagement.resolveRequest —— id 为 inbox 行的 platform_request_id。
404
+ * 先 GET_SYSTEM_MSG 按 flag(回退 seq)定位请求,再按类型路由 handle_friend/group_request。
405
+ */
406
+ async approveRequest(id, remark) {
407
+ await this.#handleSystemRequest(id, true, { remark });
408
+ }
409
+ async rejectRequest(id, reason) {
410
+ await this.#handleSystemRequest(id, false, { reason });
411
+ }
412
+ /** EndpointManagement.deleteFriend —— FRIEND_DELETE。 */
413
+ async deleteFriend(userId) {
414
+ await this.#mustRequest(Actions.FRIEND_DELETE, { user_id: toNumericId(userId, 'user_id') }, '删除好友');
415
+ }
416
+ /** deleteFriend 别名(console 探测 delete_friend)。 */
417
+ delete_friend(userId) {
418
+ return this.deleteFriend(userId);
419
+ }
420
+ /** EndpointManagement.kickGroupMember —— GROUP_KICK。 */
421
+ async removeMember(groupId, userId) {
422
+ await this.#mustRequest(Actions.GROUP_KICK, { group_id: toNumericId(groupId, 'group_id'), user_id: toNumericId(userId, 'user_id') }, '踢出群成员');
423
+ }
424
+ kickMember(groupId, userId) {
425
+ return this.removeMember(groupId, userId);
426
+ }
427
+ setGroupKick(groupId, userId) {
428
+ return this.removeMember(groupId, userId);
429
+ }
430
+ /** EndpointManagement.muteGroupMember —— GROUP_MUTE(duration 秒,默认 600)。 */
431
+ async muteMember(groupId, userId, duration = 600) {
432
+ await this.#mustRequest(Actions.GROUP_MUTE, {
433
+ group_id: toNumericId(groupId, 'group_id'),
434
+ user_id: toNumericId(userId, 'user_id'),
435
+ duration,
436
+ }, '禁言群成员');
437
+ }
438
+ banMember(groupId, userId, duration = 600) {
439
+ return this.muteMember(groupId, userId, duration);
440
+ }
441
+ setGroupMute(groupId, userId, duration = 600) {
442
+ return this.muteMember(groupId, userId, duration);
443
+ }
444
+ /** EndpointManagement.setGroupAdmin —— SET_GROUP_ADMIN(enable 默认 true)。 */
445
+ async setModerator(groupId, userId, enable = true) {
446
+ await this.#mustRequest(Actions.SET_GROUP_ADMIN, {
447
+ group_id: toNumericId(groupId, 'group_id'),
448
+ user_id: toNumericId(userId, 'user_id'),
449
+ enable,
450
+ }, '设置群管理员');
451
+ }
452
+ setAdmin(groupId, userId, enable = true) {
453
+ return this.setModerator(groupId, userId, enable);
454
+ }
455
+ setGroupAdmin(groupId, userId, enable = true) {
456
+ return this.setModerator(groupId, userId, enable);
457
+ }
458
+ /** IPC 请求并统一错误上下文:daemon 返回 ok=false 时抛出带操作名的错误。 */
459
+ async #mustRequest(action, params, label) {
460
+ const resp = await this.request(action, params);
461
+ if (!resp.ok) {
462
+ throw new Error(`icqq ${label}失败: ${resp.error ?? 'daemon 未返回错误详情'}`);
463
+ }
464
+ return resp;
465
+ }
466
+ /** 按 flag/seq 在 GET_SYSTEM_MSG 中定位请求,路由到好友/群请求处理 action。 */
467
+ async #handleSystemRequest(id, approve, extra) {
468
+ const resp = await this.#mustRequest(Actions.GET_SYSTEM_MSG, undefined, '获取待处理请求');
469
+ const data = resp.data;
470
+ const friendRequests = Array.isArray(data?.friendRequests)
471
+ ? data.friendRequests
472
+ : [];
473
+ const groupRequests = Array.isArray(data?.groupRequests)
474
+ ? data.groupRequests
475
+ : [];
476
+ const matches = (m) => m.flag === id || (m.seq != null && String(m.seq) === id);
477
+ const friend = friendRequests.find(matches);
478
+ const group = friend ? undefined : groupRequests.find(matches);
479
+ const target = friend ?? group;
480
+ if (!target?.flag) {
481
+ throw new Error(`icqq 未找到待处理请求: ${id}(GET_SYSTEM_MSG 中无匹配 flag/seq)`);
482
+ }
483
+ const params = { flag: target.flag, approve };
484
+ if (friend) {
485
+ if (extra.remark)
486
+ params.remark = extra.remark;
487
+ await this.#mustRequest(Actions.HANDLE_FRIEND_REQUEST, params, '处理好友请求');
488
+ }
489
+ else {
490
+ if (extra.reason)
491
+ params.reason = extra.reason;
492
+ await this.#mustRequest(Actions.HANDLE_GROUP_REQUEST, params, '处理群请求');
493
+ }
494
+ }
495
+ /** Test / internal: admit when open. */
496
+ admit(msg) {
497
+ if (!this.#open)
498
+ return;
499
+ // 新 Runtime Message.content 为纯文本:@ 本机(uin = name)只能经 metadata 传递
500
+ const mentioned = isIcqqBotMentioned({ uin: this.name, rawMessage: msg.content });
501
+ void this.#options.gateway.receive({
502
+ adapter: this.#options.id,
503
+ target: msg.target,
504
+ content: msg.content,
505
+ sender: msg.sender,
506
+ id: msg.id,
507
+ metadata: Object.freeze({
508
+ endpoint: this.name,
509
+ channelType: msg.channelType,
510
+ ...msg.metadata,
511
+ ...(mentioned ? { mentioned: true } : {}),
512
+ }),
513
+ }).catch((err) => {
514
+ logger.warn(formatCompact({
515
+ op: 'icqq_gateway_receive_failed',
516
+ target: msg.target,
517
+ error: err instanceof Error ? err.message : String(err),
518
+ }));
519
+ });
520
+ }
521
+ async #refreshLists() {
522
+ const [flResp, glResp] = await Promise.all([
523
+ this.ipc.request(Actions.LIST_FRIENDS),
524
+ this.ipc.request(Actions.LIST_GROUPS),
525
+ ]);
526
+ this.friends.clear();
527
+ if (flResp.ok && Array.isArray(flResp.data)) {
528
+ for (const f of flResp.data) {
529
+ this.friends.set(f.user_id, f);
530
+ }
531
+ }
532
+ this.groups.clear();
533
+ if (glResp.ok && Array.isArray(glResp.data)) {
534
+ for (const g of glResp.data) {
535
+ this.groups.set(g.group_id, g);
536
+ }
537
+ }
538
+ }
539
+ #handleEvent(event) {
540
+ if (isIcqqGuildIpcEvent(event.event)) {
541
+ this.#handleGuildEvent(event);
542
+ return;
543
+ }
544
+ const payload = unwrapIcqqIpcEventPayload(event);
545
+ if (!payload || typeof payload !== 'object')
546
+ return;
547
+ if (isIcqqRequestPayload(payload)) {
548
+ this.#recordInboxRequest(payload);
549
+ return;
550
+ }
551
+ if (isIcqqNoticePayload(payload)) {
552
+ this.#recordInboxNotice(payload);
553
+ return;
554
+ }
555
+ if (!isIcqqMessagePostType(payload))
556
+ return;
557
+ const data = payload;
558
+ if (shouldSkipSelfInboundMessage(data))
559
+ return;
560
+ const normalized = normalizeIcqqInboundMessage(data);
561
+ if (!normalized)
562
+ return;
563
+ if (!this.#inboundDeduper.shouldProcess(normalized.messageId))
564
+ return;
565
+ this.admit({
566
+ id: normalized.messageId,
567
+ target: formatInboundTarget({
568
+ channelType: normalized.channelType,
569
+ channelId: normalized.channelId,
570
+ channelParentGroupId: normalized.channelParentGroupId,
571
+ }),
572
+ content: formatInboundContent(normalized.rawMessage),
573
+ sender: normalized.userId,
574
+ channelType: normalized.channelType,
575
+ metadata: buildIcqqQuoteMetadata(data, {
576
+ nickname: normalized.nickname,
577
+ senderRole: normalized.senderRole,
578
+ }),
579
+ });
580
+ }
581
+ #handleGuildEvent(event) {
582
+ const payload = event.data;
583
+ if (!payload || typeof payload !== 'object')
584
+ return;
585
+ const normalized = normalizeIcqqGuildInboundMessage(payload);
586
+ if (!normalized)
587
+ return;
588
+ this.#guildCatalog.upsertFromInbound(normalized.raw);
589
+ if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`))
590
+ return;
591
+ this.admit({
592
+ id: normalized.messageId,
593
+ target: formatInboundTarget({
594
+ channelType: 'channel',
595
+ channelId: normalized.channelId,
596
+ guildId: normalized.guildId,
597
+ }),
598
+ content: formatInboundContent(normalized.rawMessage),
599
+ sender: normalized.userId,
600
+ channelType: 'channel',
601
+ metadata: {
602
+ guildId: normalized.guildId,
603
+ nickname: normalized.nickname,
604
+ },
605
+ });
606
+ }
607
+ /** 收件箱行公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
608
+ #inboxBase() {
609
+ const id = String(this.#options.id);
610
+ return { adapter: id.split('\0').pop() ?? id, endpointId: this.name };
611
+ }
612
+ #recordInboxRequest(payload) {
613
+ const hooks = this.#options.inbox;
614
+ if (!hooks)
615
+ return;
616
+ const row = buildIcqqInboxRequestRow(payload, this.#inboxBase());
617
+ if (!row)
618
+ return;
619
+ if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`))
620
+ return;
621
+ void hooks.recordRequest(row);
622
+ hooks.publish?.('endpoint:request', row);
623
+ }
624
+ #recordInboxNotice(payload) {
625
+ const hooks = this.#options.inbox;
626
+ if (!hooks)
627
+ return;
628
+ const row = buildIcqqInboxNoticeRow(payload, this.#inboxBase());
629
+ if (!row)
630
+ return;
631
+ if (!this.#inboxDeduper.shouldProcess(`notice:${String(row.platform_notice_id)}`))
632
+ return;
633
+ void hooks.recordNotice(row);
634
+ hooks.publish?.('endpoint:notice', row);
635
+ }
636
+ /** 启动/重连后首拉 GET_SYSTEM_MSG:补录离线期间的好友/群待处理请求。 */
637
+ async #pullPendingSystemMessages() {
638
+ const hooks = this.#options.inbox;
639
+ if (!hooks || !this.ipc)
640
+ return;
641
+ try {
642
+ const resp = await this.ipc.request(Actions.GET_SYSTEM_MSG);
643
+ if (!resp.ok)
644
+ return;
645
+ const data = resp.data;
646
+ const base = this.#inboxBase();
647
+ const rows = [
648
+ ...(Array.isArray(data?.friendRequests)
649
+ ? data.friendRequests.map((m) => buildIcqqSystemRequestRow(m, 'friend', base))
650
+ : []),
651
+ ...(Array.isArray(data?.groupRequests)
652
+ ? data.groupRequests.map((m) => buildIcqqSystemRequestRow(m, 'group', base))
653
+ : []),
654
+ ];
655
+ for (const row of rows) {
656
+ if (!row)
657
+ continue;
658
+ if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`))
659
+ continue;
660
+ void hooks.recordRequest(row);
661
+ hooks.publish?.('endpoint:request', row);
662
+ }
663
+ }
664
+ catch (error) {
665
+ logger.debug(formatCompact({
666
+ op: 'inbox_pull_system_msg',
667
+ endpoint: this.name,
668
+ error: error instanceof Error ? error.message : String(error),
669
+ }));
670
+ }
671
+ }
672
+ }
673
+ /**
674
+ * 组装入站 metadata:在基础字段上补充 quote 链路信息。
675
+ * - quote_id:被引用消息 id(resolveIcqqQuoteIdFromEvent,无则不写)
676
+ * - quote_sender_id / quote_sender_name / quote_content:source 可用时的平铺摘要
677
+ */
678
+ function buildIcqqQuoteMetadata(data, base) {
679
+ const metadata = { ...base };
680
+ const quoteId = resolveIcqqQuoteIdFromEvent(data);
681
+ if (quoteId)
682
+ metadata.quote_id = quoteId;
683
+ const quoted = quotedPayloadFromIcqqSource(findIcqqNestedMessageSource(data));
684
+ if (quoted?.sender?.id)
685
+ metadata.quote_sender_id = quoted.sender.id;
686
+ if (quoted?.sender?.name)
687
+ metadata.quote_sender_name = quoted.sender.name;
688
+ const quoteText = quoted
689
+ ? (typeof quoted.content === 'string'
690
+ ? quoted.content
691
+ : quoted.content
692
+ .map((seg) => seg && typeof seg === 'object' && seg.type === 'text'
693
+ ? String(seg.data?.text ?? '')
694
+ : '')
695
+ .join('')).trim() || quoted.raw || ''
696
+ : '';
697
+ if (quoteText)
698
+ metadata.quote_content = quoteText;
699
+ return metadata;
700
+ }
701
+ /** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
702
+ function toNumericId(value, label) {
703
+ const n = Number(value);
704
+ if (!Number.isFinite(n) || String(value).trim() === '') {
705
+ throw new TypeError(`icqq ${label} 必须是数字: ${String(value)}`);
706
+ }
707
+ return n;
708
+ }
709
+ async function defaultCreateIpc(config) {
710
+ const uin = Number(config.name);
711
+ if (config.rpc) {
712
+ return IpcClient.connectRpc(config.rpc);
713
+ }
714
+ return IpcClient.connect(uin);
715
+ }