@zhin.js/adapter-icqq 6.0.0 → 6.0.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 (161) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +3 -3
  3. package/{skills/icqq → agent}/PERMITS.md +6 -6
  4. package/agent/tools/announce.ts +21 -0
  5. package/agent/tools/essence.ts +22 -0
  6. package/agent/tools/friend_list.ts +19 -0
  7. package/agent/tools/group_files.ts +27 -0
  8. package/agent/tools/group_invite.ts +20 -0
  9. package/agent/tools/group_list.ts +20 -0
  10. package/agent/tools/group_sign.ts +19 -0
  11. package/agent/tools/list_muted.ts +22 -0
  12. package/agent/tools/poke.ts +20 -0
  13. package/agent/tools/send_user_like.ts +22 -0
  14. package/agent/tools/set_anonymous.ts +22 -0
  15. package/agent/tools/set_title.ts +25 -0
  16. package/dist/index.js +2 -2
  17. package/lib/agent/tools/announce.js +21 -0
  18. package/lib/agent/tools/announce.js.map +1 -0
  19. package/lib/agent/tools/essence.js +22 -0
  20. package/lib/agent/tools/essence.js.map +1 -0
  21. package/lib/agent/tools/friend_list.js +19 -0
  22. package/lib/agent/tools/friend_list.js.map +1 -0
  23. package/lib/agent/tools/group_files.js +29 -0
  24. package/lib/agent/tools/group_files.js.map +1 -0
  25. package/lib/agent/tools/group_invite.js +20 -0
  26. package/lib/agent/tools/group_invite.js.map +1 -0
  27. package/lib/agent/tools/group_list.js +20 -0
  28. package/lib/agent/tools/group_list.js.map +1 -0
  29. package/lib/agent/tools/group_sign.js +19 -0
  30. package/lib/agent/tools/group_sign.js.map +1 -0
  31. package/lib/agent/tools/list_muted.js +23 -0
  32. package/lib/agent/tools/list_muted.js.map +1 -0
  33. package/lib/agent/tools/poke.js +20 -0
  34. package/lib/agent/tools/poke.js.map +1 -0
  35. package/lib/agent/tools/send_user_like.js +22 -0
  36. package/lib/agent/tools/send_user_like.js.map +1 -0
  37. package/lib/agent/tools/set_anonymous.js +22 -0
  38. package/lib/agent/tools/set_anonymous.js.map +1 -0
  39. package/lib/agent/tools/set_title.js +25 -0
  40. package/lib/agent/tools/set_title.js.map +1 -0
  41. package/lib/{adapter.js → src/adapter.js} +13 -6
  42. package/lib/src/adapter.js.map +1 -0
  43. package/lib/src/agent-prompt.js.map +1 -0
  44. package/lib/{cq-message.js → src/cq-message.js} +12 -3
  45. package/lib/src/cq-message.js.map +1 -0
  46. package/lib/{endpoint.js → src/endpoint.js} +130 -63
  47. package/lib/src/endpoint.js.map +1 -0
  48. package/lib/{forward-msg.js → src/forward-msg.js} +4 -1
  49. package/lib/src/forward-msg.js.map +1 -0
  50. package/lib/src/get-msg.js.map +1 -0
  51. package/lib/src/icqq-agent-deps.js +10 -0
  52. package/lib/src/icqq-agent-deps.js.map +1 -0
  53. package/lib/src/icqq-guild.js +115 -0
  54. package/lib/src/icqq-guild.js.map +1 -0
  55. package/lib/{icqq-inbound.js → src/icqq-inbound.js} +15 -5
  56. package/lib/src/icqq-inbound.js.map +1 -0
  57. package/lib/src/icqq-login-assist-bridge.js.map +1 -0
  58. package/lib/{icqq-side-events.js → src/icqq-side-events.js} +28 -92
  59. package/lib/src/icqq-side-events.js.map +1 -0
  60. package/lib/{index.js → src/index.js} +0 -1
  61. package/lib/src/index.js.map +1 -0
  62. package/lib/src/ipc-client.js.map +1 -0
  63. package/lib/src/login-assist-routes.js.map +1 -0
  64. package/lib/src/login-ipc-contract.js.map +1 -0
  65. package/lib/src/outbound-media.js.map +1 -0
  66. package/lib/src/protocol.js.map +1 -0
  67. package/lib/src/routes.js.map +1 -0
  68. package/lib/src/segment-mapper.js +2 -0
  69. package/lib/src/segment-mapper.js.map +1 -0
  70. package/lib/src/tools/index.js +23 -0
  71. package/lib/src/tools/index.js.map +1 -0
  72. package/lib/src/types.js.map +1 -0
  73. package/package.json +25 -21
  74. package/src/adapter.ts +13 -6
  75. package/src/agent-prompt.ts +1 -2
  76. package/src/cq-message.ts +11 -3
  77. package/src/endpoint.ts +158 -82
  78. package/src/forward-msg.ts +2 -2
  79. package/src/icqq-agent-deps.ts +18 -0
  80. package/src/icqq-guild.ts +157 -0
  81. package/src/icqq-inbound.ts +16 -5
  82. package/src/icqq-side-events.ts +33 -118
  83. package/src/index.ts +0 -8
  84. package/src/segment-mapper.ts +1 -0
  85. package/src/tools/index.ts +16 -397
  86. package/src/types.ts +0 -45
  87. package/lib/adapter.d.ts +0 -39
  88. package/lib/adapter.d.ts.map +0 -1
  89. package/lib/adapter.js.map +0 -1
  90. package/lib/agent-prompt.d.ts +0 -3
  91. package/lib/agent-prompt.d.ts.map +0 -1
  92. package/lib/agent-prompt.js.map +0 -1
  93. package/lib/cq-message.d.ts +0 -10
  94. package/lib/cq-message.d.ts.map +0 -1
  95. package/lib/cq-message.js.map +0 -1
  96. package/lib/endpoint.d.ts +0 -99
  97. package/lib/endpoint.d.ts.map +0 -1
  98. package/lib/endpoint.js.map +0 -1
  99. package/lib/forward-msg.d.ts +0 -27
  100. package/lib/forward-msg.d.ts.map +0 -1
  101. package/lib/forward-msg.js.map +0 -1
  102. package/lib/get-msg.d.ts +0 -3
  103. package/lib/get-msg.d.ts.map +0 -1
  104. package/lib/get-msg.js.map +0 -1
  105. package/lib/icqq-inbound.d.ts +0 -130
  106. package/lib/icqq-inbound.d.ts.map +0 -1
  107. package/lib/icqq-inbound.js.map +0 -1
  108. package/lib/icqq-login-assist-bridge.d.ts +0 -5
  109. package/lib/icqq-login-assist-bridge.d.ts.map +0 -1
  110. package/lib/icqq-login-assist-bridge.js.map +0 -1
  111. package/lib/icqq-side-events.d.ts +0 -34
  112. package/lib/icqq-side-events.d.ts.map +0 -1
  113. package/lib/icqq-side-events.js.map +0 -1
  114. package/lib/index.d.ts +0 -20
  115. package/lib/index.d.ts.map +0 -1
  116. package/lib/index.js.map +0 -1
  117. package/lib/ipc-client.d.ts +0 -74
  118. package/lib/ipc-client.d.ts.map +0 -1
  119. package/lib/ipc-client.js.map +0 -1
  120. package/lib/login-assist-routes.d.ts +0 -7
  121. package/lib/login-assist-routes.d.ts.map +0 -1
  122. package/lib/login-assist-routes.js.map +0 -1
  123. package/lib/login-ipc-contract.d.ts +0 -44
  124. package/lib/login-ipc-contract.d.ts.map +0 -1
  125. package/lib/login-ipc-contract.js.map +0 -1
  126. package/lib/outbound-media.d.ts +0 -14
  127. package/lib/outbound-media.d.ts.map +0 -1
  128. package/lib/outbound-media.js.map +0 -1
  129. package/lib/protocol.d.ts +0 -198
  130. package/lib/protocol.d.ts.map +0 -1
  131. package/lib/protocol.js.map +0 -1
  132. package/lib/routes.d.ts +0 -8
  133. package/lib/routes.d.ts.map +0 -1
  134. package/lib/routes.js.map +0 -1
  135. package/lib/tools/index.d.ts +0 -10
  136. package/lib/tools/index.d.ts.map +0 -1
  137. package/lib/tools/index.js +0 -341
  138. package/lib/tools/index.js.map +0 -1
  139. package/lib/types.d.ts +0 -117
  140. package/lib/types.d.ts.map +0 -1
  141. package/lib/types.js.map +0 -1
  142. package/lib/typing-indicator-example.d.ts +0 -108
  143. package/lib/typing-indicator-example.d.ts.map +0 -1
  144. package/lib/typing-indicator-example.js +0 -220
  145. package/lib/typing-indicator-example.js.map +0 -1
  146. package/lib/typing-indicator.d.ts +0 -87
  147. package/lib/typing-indicator.d.ts.map +0 -1
  148. package/lib/typing-indicator.js +0 -229
  149. package/lib/typing-indicator.js.map +0 -1
  150. package/src/typing-indicator-example.ts +0 -269
  151. package/src/typing-indicator.ts +0 -317
  152. /package/lib/{agent-prompt.js → src/agent-prompt.js} +0 -0
  153. /package/lib/{get-msg.js → src/get-msg.js} +0 -0
  154. /package/lib/{icqq-login-assist-bridge.js → src/icqq-login-assist-bridge.js} +0 -0
  155. /package/lib/{ipc-client.js → src/ipc-client.js} +0 -0
  156. /package/lib/{login-assist-routes.js → src/login-assist-routes.js} +0 -0
  157. /package/lib/{login-ipc-contract.js → src/login-ipc-contract.js} +0 -0
  158. /package/lib/{outbound-media.js → src/outbound-media.js} +0 -0
  159. /package/lib/{protocol.js → src/protocol.js} +0 -0
  160. /package/lib/{routes.js → src/routes.js} +0 -0
  161. /package/lib/{types.js → src/types.js} +0 -0
package/src/endpoint.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * 不再直接依赖 @icqqjs/icqq 协议库。
5
5
  * 登录由 `icqq login` 完成,本 Endpoint 只负责连接守护进程并收发消息。
6
6
  */
7
- import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, type QuotedMessagePayload,} from 'zhin.js';
7
+ import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, expandInteractiveSegmentsInContent, resolveSideEventDedupeKey, type QuotedMessagePayload,} from 'zhin.js';
8
8
  import type {
9
9
  IcqqEndpointConfig,
10
10
  IcqqSenderInfo,
@@ -14,20 +14,9 @@ import type {
14
14
  } from "./types.js";
15
15
  import type { IcqqAdapter } from "./adapter.js";
16
16
  import { IpcClient } from "./ipc-client.js";
17
- import { Actions, type IpcEvent } from "./protocol.js";
18
- import type { IcqqIpcMessageEvent } from "./icqq-inbound.js";
19
- import {
20
- findIcqqNestedMessageSource,
21
- InboundMessageDeduper,
22
- isIcqqMessagePostType,
23
- normalizeIcqqInboundMessage,
24
- quotedPayloadFromIcqqSource,
25
- resolveIcqqQuoteIdFromEvent,
26
- resolveQuoteIdFromIcqqSource,
27
- shouldSkipSelfInboundMessage,
28
- unwrapIcqqIpcEventPayload,
29
- type NormalizedIcqqInbound,
30
- } from "./icqq-inbound.js";
17
+ import { Actions, type IpcEvent, type IpcGuildMessageEventData } from './protocol.js';
18
+ import { findIcqqNestedMessageSource, InboundMessageDeduper, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveQuoteIdFromIcqqSource, shouldSkipSelfInboundMessage, unwrapIcqqIpcEventPayload, type IcqqIpcMessageEvent, type NormalizedIcqqInbound } from './icqq-inbound.js';
19
+
31
20
  import {
32
21
  buildIcqqIpcMessage as buildIcqqIpcMessageImpl,
33
22
  parseCqMessage as parseCqMessageImpl,
@@ -45,7 +34,6 @@ import {
45
34
  isIcqqNoticePayload,
46
35
  isIcqqRequestPayload,
47
36
  resolveIcqqEventPostType,
48
- resolveSideEventDedupeKey,
49
37
  shouldRefreshListsOnMeta,
50
38
  type IcqqIpcRawEvent,
51
39
  } from "./icqq-side-events.js";
@@ -56,9 +44,11 @@ import {
56
44
  import { parseIcqqGetMsgResponse } from "./get-msg.js";
57
45
  import { enrichQuotedPayloadWithForward, isForwardPlaceholderPayload } from "./forward-msg.js";
58
46
  import {
59
- enableTypingIndicator,
60
- type ICQQTypingIndicatorManager,
61
- } from "./typing-indicator.js";
47
+ IcqqGuildCatalog,
48
+ isIcqqGuildIpcEvent,
49
+ normalizeIcqqGuildInboundMessage,
50
+ type NormalizedIcqqGuildInbound,
51
+ } from "./icqq-guild.js";
62
52
 
63
53
  export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessageEvent> {
64
54
  $connected = false;
@@ -69,6 +59,8 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
69
59
  friends = new Map<number, IpcFriendInfo>();
70
60
  /** 缓存的群列表 */
71
61
  groups = new Map<number, IpcGroupInfo>();
62
+ /** QQ 频道(guild)子频道缓存 */
63
+ readonly guildCatalog = new IcqqGuildCatalog();
72
64
 
73
65
  private subscriptions: Array<{ unsubscribe: () => Promise<void> }> = [];
74
66
  /** 事件去重:覆盖多端回流/服务端重复推送等场景 */
@@ -80,9 +72,6 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
80
72
  /** 是否已有重连循环在跑(避免多次 schedule 叠套) */
81
73
  private reconnectRunning = false;
82
74
 
83
- /** Typing Indicator 管理器 */
84
- $typingIndicator?: ICQQTypingIndicatorManager;
85
-
86
75
  get $id() {
87
76
  return this.$config.name;
88
77
  }
@@ -113,50 +102,13 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
113
102
 
114
103
  await this.rebindIpcSession();
115
104
 
116
- // 根据配置自动启用 Typing Indicator
117
- this.initTypingIndicator();
118
-
119
- this.logger.info(formatCompact( {
105
+ this.logger.debug(formatCompact( {
120
106
  机器人: this.$id,
121
107
  好友数: this.friends.size,
122
108
  群组数: this.groups.size,
123
- 思考提示: this.$typingIndicator ? '已启用' : '未启用',
124
109
  }));
125
110
  }
126
111
 
127
- /**
128
- * 初始化 Typing Indicator
129
- * 根据配置自动启用或禁用
130
- */
131
- private initTypingIndicator(): void {
132
- const typingConfig = this.$config.typingIndicator;
133
-
134
- // 如果配置中明确设置为 false,则不启用
135
- if (typingConfig?.enabled === false) {
136
- this.logger.debug(formatCompact({
137
- endpoint: this.$id,
138
- typingIndicator: 'disabled_by_config',
139
- }));
140
- return;
141
- }
142
-
143
- // 启用 Typing Indicator
144
- try {
145
- this.$typingIndicator = enableTypingIndicator(this, typingConfig);
146
- this.logger.debug(formatCompact({
147
- endpoint: this.$id,
148
- typingIndicator: 'enabled',
149
- emoji: typingConfig?.defaultEmoji || '⏳',
150
- }));
151
- } catch (error) {
152
- this.logger.warn(formatCompact({
153
- endpoint: this.$id,
154
- typingIndicator: 'failed',
155
- error: error instanceof Error ? error.message : String(error),
156
- }));
157
- }
158
- }
159
-
160
112
  /** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
161
113
  private async rebindIpcSession(): Promise<void> {
162
114
  const uin = Number(this.$config.name);
@@ -227,7 +179,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
227
179
 
228
180
  try {
229
181
  await this.rebindIpcSession();
230
- this.logger.info(formatCompact( {
182
+ this.logger.debug(formatCompact( {
231
183
  op: "reconnect",
232
184
  endpoint: this.$id,
233
185
  friends: this.friends.size,
@@ -268,6 +220,27 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
268
220
  this.groups.set(g.group_id, g);
269
221
  }
270
222
  }
223
+
224
+ await this.guildCatalog.syncAll(this.ipc).catch((e: unknown) => {
225
+ this.logger.warn(formatCompact({
226
+ op: "sync_guilds",
227
+ endpoint: this.$id,
228
+ ok: false,
229
+ error: e instanceof Error ? e.message : String(e),
230
+ }));
231
+ });
232
+ }
233
+
234
+ /** Console:列出 guild 子频道(含 parent.guild) */
235
+ getGuildChannelList(): ReturnType<IcqqGuildCatalog["getGuildChannelList"]> {
236
+ return this.guildCatalog.getGuildChannelList();
237
+ }
238
+
239
+ resolveConsoleChannelNames(
240
+ channelId: string,
241
+ guildId?: string,
242
+ ): { channelName?: string; parentName?: string } {
243
+ return this.guildCatalog.resolveConsoleChannelNames(channelId, guildId);
271
244
  }
272
245
 
273
246
  // ── 断开 ───────────────────────────────────────────────────────────
@@ -275,12 +248,6 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
275
248
  async $disconnect(): Promise<void> {
276
249
  this.intentionalDisconnect = true;
277
250
 
278
- // 停止所有 Typing Indicator
279
- if (this.$typingIndicator) {
280
- await this.$typingIndicator.stopAll().catch(() => {});
281
- this.$typingIndicator = undefined;
282
- }
283
-
284
251
  this.ipc?.setOnRemoteDisconnect(null);
285
252
  for (const sub of this.subscriptions) {
286
253
  await sub.unsubscribe().catch(() => {});
@@ -290,7 +257,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
290
257
  disposeIcqqLoginAssistBridge(this.$id);
291
258
  this.ipc?.close();
292
259
  this.$connected = false;
293
- this.logger.info(formatCompact( { op: "disconnect", endpoint: this.$id }));
260
+ this.logger.debug(formatCompact( { op: "disconnect", endpoint: this.$id }));
294
261
  }
295
262
 
296
263
  // ── 消息处理 ───────────────────────────────────────────────────────
@@ -301,6 +268,23 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
301
268
  return;
302
269
  }
303
270
 
271
+ if (event.event === Actions.RELOAD_GUILDS) {
272
+ void this.guildCatalog.syncAll(this.ipc).catch((e: unknown) => {
273
+ this.logger.warn(formatCompact({
274
+ op: "reload_guilds",
275
+ endpoint: this.$id,
276
+ ok: false,
277
+ error: e instanceof Error ? e.message : String(e),
278
+ }));
279
+ });
280
+ return;
281
+ }
282
+
283
+ if (isIcqqGuildIpcEvent(event.event)) {
284
+ this.handleGuildMessageEvent(event);
285
+ return;
286
+ }
287
+
304
288
  const payload = unwrapIcqqIpcEventPayload(event);
305
289
  if (!payload || typeof payload !== "object") {
306
290
  if (process.env.ICQQ_IPC_LOG_RAW === "1") {
@@ -351,7 +335,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
351
335
 
352
336
  if (shouldSkipSelfInboundMessage(data)) {
353
337
  if (process.env.ICQQ_IPC_LOG_RAW === "1") {
354
- this.logger.info(formatCompact({ ipc_skip: "self_message" }));
338
+ this.logger.debug(formatCompact({ ipc_skip: "self_message" }));
355
339
  }
356
340
  return;
357
341
  }
@@ -359,7 +343,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
359
343
  const normalized = normalizeIcqqInboundMessage(data);
360
344
  if (!normalized) {
361
345
  if (process.env.ICQQ_IPC_LOG_RAW === "1") {
362
- this.logger.info(formatCompact({ ipc_skip: "normalize_failed" }));
346
+ this.logger.debug(formatCompact({ ipc_skip: "normalize_failed" }));
363
347
  }
364
348
  return;
365
349
  }
@@ -393,22 +377,95 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
393
377
  );
394
378
  }
395
379
 
380
+ private handleGuildMessageEvent(event: IpcEvent): void {
381
+ const payload = event.data;
382
+ if (!payload || typeof payload !== "object") return;
383
+ const data = payload as IpcGuildMessageEventData & Record<string, unknown>;
384
+ const normalized = normalizeIcqqGuildInboundMessage(data);
385
+ if (!normalized) return;
386
+
387
+ this.guildCatalog.upsertFromInbound(data);
388
+
389
+ if (!this.inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) {
390
+ return;
391
+ }
392
+
393
+ void this.dispatchGuildInboundMessage(normalized);
394
+ }
395
+
396
+ private async dispatchGuildInboundMessage(
397
+ normalized: NormalizedIcqqGuildInbound,
398
+ ): Promise<void> {
399
+ const message = this.$formatGuildMessage(normalized);
400
+ this.adapter.emit("message.receive", message);
401
+ this.logger.debug(
402
+ `${this.$id} recv channel(${message.$channel.id}):${segment.raw(message.$content)}`,
403
+ );
404
+ }
405
+
406
+ private $formatGuildMessage(
407
+ normalized: NormalizedIcqqGuildInbound,
408
+ ): ReturnType<typeof Message.from<IpcGuildMessageEventData>> {
409
+ const senderInfo: IcqqSenderInfo = {
410
+ id: normalized.userId,
411
+ name: normalized.nickname,
412
+ };
413
+
414
+ const result = Message.from(normalized.raw, {
415
+ $id: normalized.messageId,
416
+ $adapter: "icqq" as const,
417
+ $endpoint: this.$config.name,
418
+ $sender: senderInfo,
419
+ $channel: {
420
+ id: normalized.channelId,
421
+ type: "channel",
422
+ parent: { type: "guild", id: normalized.guildId },
423
+ },
424
+ $content: normalized.content,
425
+ $raw: normalized.rawMessage,
426
+ $timestamp: normalized.timestampMs,
427
+ $recall: async () => {
428
+ await this.$recallMessage(result.$id);
429
+ },
430
+ $reply: async (
431
+ content: SendContent,
432
+ quote?: boolean | string,
433
+ ): Promise<string> => {
434
+ if (!Array.isArray(content)) content = [content];
435
+ if (quote) {
436
+ content.unshift({
437
+ type: "reply",
438
+ data: { id: typeof quote === "boolean" ? result.$id : quote },
439
+ });
440
+ }
441
+ return await this.adapter.sendMessage({
442
+ context: "icqq",
443
+ endpoint: this.$config.name,
444
+ id: normalized.channelId,
445
+ type: "channel",
446
+ parent: { type: "guild", id: normalized.guildId },
447
+ content,
448
+ });
449
+ },
450
+ });
451
+ return result;
452
+ }
453
+
396
454
  private handleNoticeEvent(event: IcqqIpcRawEvent): void {
397
455
  const dedupeKey = resolveSideEventDedupeKey(event, "notice");
398
456
  if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
399
457
  if (process.env.ICQQ_IPC_LOG_RAW === "1") {
400
- this.logger.info(formatCompact({ ipc_dedupe: dedupeKey }));
458
+ this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
401
459
  }
402
460
  return;
403
461
  }
404
462
  const notice = formatIcqqNotice(event, this.$config.name);
405
463
  this.adapter.emit("notice.receive", notice);
406
- this.logger.info(
464
+ this.logger.debug(
407
465
  formatCompact({
408
- notice: notice.$type,
409
- channel: `${notice.$channel.type}(${notice.$channel.id})`,
466
+ notice: `${notice.$type}.${notice.$scene_type}.${notice.$sub_type}`,
467
+ scene: `${notice.$scene_type}(${notice.$scene_id})`,
410
468
  endpoint: this.$id,
411
- sub_type: notice.$subType,
412
469
  }),
413
470
  );
414
471
  }
@@ -417,24 +474,26 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
417
474
  const dedupeKey = resolveSideEventDedupeKey(event, "request");
418
475
  if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
419
476
  if (process.env.ICQQ_IPC_LOG_RAW === "1") {
420
- this.logger.info(formatCompact({ ipc_dedupe: dedupeKey }));
477
+ this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
421
478
  }
422
479
  return;
423
480
  }
424
481
  const request = formatIcqqRequest(event, this.$config.name, this.ipc);
425
482
  this.adapter.emit("request.receive", request);
426
- this.logger.info(
483
+ this.logger.debug(
427
484
  formatCompact({
428
- request: request.$type,
429
- channel: `${request.$channel.type}(${request.$channel.id})`,
485
+ request: `${request.$type}.${request.$scene_type}.${request.$sub_type}`,
486
+ scene: `${request.$scene_type}(${request.$scene_id})`,
430
487
  endpoint: this.$id,
431
- from: request.$sender.id,
488
+ from: request.$actor.id,
432
489
  }),
433
490
  );
434
491
  }
435
492
 
436
493
  private handleMetaEvent(event: IcqqIpcRawEvent): void {
437
- const dedupeKey = resolveSideEventDedupeKey(event, "meta");
494
+ const time = event.time ?? 0;
495
+ const kind = event.meta_event_type ?? event.post_type ?? 'meta';
496
+ const dedupeKey = `meta:${time}_${kind}_${event.sub_type ?? ''}`;
438
497
  if (!this.inboundDeduper.shouldProcess(dedupeKey)) return;
439
498
 
440
499
  this.logger.debug(formatIcqqMetaLog(event));
@@ -657,7 +716,8 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
657
716
 
658
717
  async $sendMessage(options: SendOptions): Promise<string> {
659
718
  const outboundMedia = resolveIcqqOutboundMediaMode(this.$config);
660
- const qrcodeContent = options.content;
719
+ const expanded = expandInteractiveSegmentsInContent(options.content);
720
+ const qrcodeContent = expanded;
661
721
  const content = materializeOutboundBase64(qrcodeContent, outboundMedia);
662
722
  const message = buildIcqqIpcMessageImpl(content);
663
723
 
@@ -685,6 +745,22 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
685
745
  action = Actions.SEND_GROUP_MSG;
686
746
  params = { group_id: Number(options.id), message };
687
747
  break;
748
+ case "channel": {
749
+ const guildId =
750
+ options.parent?.type === "guild" ? options.parent.id : undefined;
751
+ if (!guildId) {
752
+ throw new Error(
753
+ 'channel 发送需要 parent: { type: "guild", id: guild_id }',
754
+ );
755
+ }
756
+ action = Actions.GUILD_SEND_MSG;
757
+ params = {
758
+ guild_id: guildId,
759
+ channel_id: options.id,
760
+ message,
761
+ };
762
+ break;
763
+ }
688
764
  default:
689
765
  throw new Error(`不支持的频道类型: ${options.type}`);
690
766
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * 合并转发(聊天记录)解析:从 json/xml 提取 resid,经 IPC get_forward_msg 拉取正文。
3
3
  */
4
- import type { MessageSegment, QuotedMessagePayload } from "zhin.js";
5
- import { segment } from "zhin.js";
4
+ import { segment, type MessageSegment, type QuotedMessagePayload } from 'zhin.js';
5
+
6
6
  import { Actions } from "./protocol.js";
7
7
  import type { IpcClient } from "./ipc-client.js";
8
8
  import {
@@ -0,0 +1,18 @@
1
+ import type { IcqqAdapter } from './adapter.js';
2
+ import type { IcqqEndpoint } from './endpoint.js';
3
+
4
+ export interface IcqqAgentDeps {
5
+ getEndpoint: (endpointId: string) => IcqqEndpoint;
6
+ getAdapter: () => IcqqAdapter;
7
+ }
8
+
9
+ let _deps: IcqqAgentDeps | null = null;
10
+
11
+ export function setIcqqAgentDeps(deps: IcqqAgentDeps): void {
12
+ _deps = deps;
13
+ }
14
+
15
+ export function getIcqqAgentDeps(): IcqqAgentDeps {
16
+ if (!_deps) throw new Error('icqq agent deps not initialized');
17
+ return _deps;
18
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * ICQQ guild (QQ 频道) catalog sync, inbound normalize, Console channel list.
3
+ */
4
+ import type { MessageSegment } from "zhin.js";
5
+ import { Actions, type IpcGuildMessageEventData } from "./protocol.js";
6
+ import type { IpcClient } from "./ipc-client.js";
7
+ import { parseCqMessage } from "./cq-message.js";
8
+ import { toCanonicalSegments } from "./segment-mapper.js";
9
+
10
+ export interface IcqqGuildChannelEntry {
11
+ channel_id: string;
12
+ channel_name: string;
13
+ guild_id: string;
14
+ guild_name: string;
15
+ }
16
+
17
+ export interface NormalizedIcqqGuildInbound {
18
+ messageId: string;
19
+ channelId: string;
20
+ guildId: string;
21
+ guildName: string;
22
+ channelName: string;
23
+ userId: string;
24
+ nickname: string;
25
+ content: MessageSegment[];
26
+ rawMessage: string;
27
+ timestampMs: number;
28
+ raw: IpcGuildMessageEventData;
29
+ }
30
+
31
+ export class IcqqGuildCatalog {
32
+ private readonly channels = new Map<string, IcqqGuildChannelEntry>();
33
+ private readonly guilds = new Map<string, { guild_id: string; guild_name: string }>();
34
+
35
+ clear(): void {
36
+ this.channels.clear();
37
+ this.guilds.clear();
38
+ }
39
+
40
+ async syncAll(ipc: IpcClient): Promise<void> {
41
+ const listResp = await ipc.request(Actions.GUILD_LIST);
42
+ if (!listResp.ok) return;
43
+
44
+ const guildRows = Array.isArray(listResp.data) ? listResp.data : [];
45
+ for (const row of guildRows) {
46
+ const rec = row as Record<string, unknown>;
47
+ const guild_id = String(rec.guild_id ?? rec.id ?? "");
48
+ if (!guild_id) continue;
49
+ const guild_name = String(rec.guild_name ?? rec.name ?? guild_id);
50
+ this.guilds.set(guild_id, { guild_id, guild_name });
51
+
52
+ const chResp = await ipc.request(Actions.GUILD_CHANNELS, { guild_id });
53
+ if (!chResp.ok || !Array.isArray(chResp.data)) continue;
54
+ for (const ch of chResp.data) {
55
+ const chRec = ch as Record<string, unknown>;
56
+ const channel_id = String(chRec.channel_id ?? chRec.id ?? "");
57
+ if (!channel_id) continue;
58
+ const channel_name = String(chRec.channel_name ?? chRec.name ?? channel_id);
59
+ this.channels.set(channel_id, {
60
+ channel_id,
61
+ channel_name,
62
+ guild_id,
63
+ guild_name,
64
+ });
65
+ }
66
+ }
67
+ }
68
+
69
+ upsertFromInbound(data: IpcGuildMessageEventData): void {
70
+ const guild_id = String(data.guild_id ?? "");
71
+ const channel_id = String(data.channel_id ?? "");
72
+ if (!guild_id || !channel_id) return;
73
+ const guild_name = String(data.guild_name ?? guild_id);
74
+ const channel_name = String(data.channel_name ?? channel_id);
75
+ this.guilds.set(guild_id, { guild_id, guild_name });
76
+ this.channels.set(channel_id, {
77
+ channel_id,
78
+ channel_name,
79
+ guild_id,
80
+ guild_name,
81
+ });
82
+ }
83
+
84
+ getGuildChannelList(): Array<{
85
+ id: string;
86
+ name: string;
87
+ parent: { type: "guild"; id: string; name: string };
88
+ }> {
89
+ return Array.from(this.channels.values()).map((entry) => ({
90
+ id: entry.channel_id,
91
+ name: entry.channel_name,
92
+ parent: {
93
+ type: "guild" as const,
94
+ id: entry.guild_id,
95
+ name: entry.guild_name,
96
+ },
97
+ }));
98
+ }
99
+
100
+ resolveConsoleChannelNames(
101
+ channelId: string,
102
+ guildId?: string,
103
+ ): { channelName?: string; parentName?: string } {
104
+ const entry = this.channels.get(channelId);
105
+ if (entry) {
106
+ return { channelName: entry.channel_name, parentName: entry.guild_name };
107
+ }
108
+ if (guildId) {
109
+ const guild = this.guilds.get(guildId);
110
+ return guild ? { parentName: guild.guild_name } : {};
111
+ }
112
+ return {};
113
+ }
114
+ }
115
+
116
+ export function isIcqqGuildIpcEvent(eventName: string | undefined): boolean {
117
+ return typeof eventName === "string" && eventName.startsWith("message.guild");
118
+ }
119
+
120
+ export function normalizeIcqqGuildInboundMessage(
121
+ data: IpcGuildMessageEventData & Record<string, unknown>,
122
+ ): NormalizedIcqqGuildInbound | null {
123
+ if (data.type !== "guild") return null;
124
+ const guildId = String(data.guild_id ?? "");
125
+ const channelId = String(data.channel_id ?? "");
126
+ if (!guildId || !channelId) return null;
127
+
128
+ const userId = String(data.tiny_id ?? data.user_id ?? "");
129
+ const nickname = String(data.nickname ?? userId);
130
+ const rawMessage = String(data.raw_message ?? "");
131
+ const timestampMs =
132
+ typeof data.time === "number" ? data.time * 1000 : Date.now();
133
+ const messageId =
134
+ data.message_id != null && String(data.message_id) !== ""
135
+ ? String(data.message_id)
136
+ : data.seq != null
137
+ ? String(data.seq)
138
+ : `${data.time ?? 0}_${userId}_${channelId}`;
139
+
140
+ const parsed = parseCqMessage(rawMessage);
141
+ const canonical = toCanonicalSegments(parsed);
142
+ const content: MessageSegment[] = canonical.length > 0 ? canonical : [{ type: "text", data: { text: rawMessage } }];
143
+
144
+ return {
145
+ messageId,
146
+ channelId,
147
+ guildId,
148
+ guildName: String(data.guild_name ?? guildId),
149
+ channelName: String(data.channel_name ?? channelId),
150
+ userId,
151
+ nickname,
152
+ content,
153
+ rawMessage,
154
+ timestampMs,
155
+ raw: data,
156
+ };
157
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { Message, type MessageSegment, type QuotedMessagePayload } from "zhin.js";
5
5
  import { parseCqMessage } from "./cq-message.js";
6
+ import { toCanonicalSegments } from "./segment-mapper.js";
6
7
  import { extractForwardResidFromJsonElement } from "./forward-msg.js";
7
8
  import type { GroupRole } from "./types.js";
8
9
 
@@ -292,12 +293,22 @@ export function icqqElementsToSegments(
292
293
  out.push({ type: "text", data: { text: String(el.text) } });
293
294
  }
294
295
  break;
295
- case "at":
296
+ case "at": {
297
+ const atId = String(el.qq ?? el.user_id ?? "");
298
+ const name =
299
+ typeof el.name === "string"
300
+ ? el.name
301
+ : typeof el.nickname === "string"
302
+ ? el.nickname
303
+ : undefined;
296
304
  out.push({
297
305
  type: "at",
298
- data: { qq: String(el.qq ?? el.user_id ?? "") },
306
+ data: name
307
+ ? { qq: atId, user_id: atId, id: atId, name }
308
+ : { qq: atId, user_id: atId, id: atId },
299
309
  });
300
310
  break;
311
+ }
301
312
  case "face":
302
313
  out.push({ type: "face", data: { id: Number(el.id) } });
303
314
  break;
@@ -584,7 +595,7 @@ function mergeReplyFromRawMessage(
584
595
  const quoteFromRaw = Message.quoteIdFromContent(fromRaw);
585
596
  if (!quoteFromRaw) return content;
586
597
  return [
587
- { type: "reply", data: { id: quoteFromRaw, message_id: quoteFromRaw } },
598
+ { type: "reply", data: { message_id: quoteFromRaw } },
588
599
  ...content.filter((s) => s.type !== "reply"),
589
600
  ];
590
601
  }
@@ -599,7 +610,7 @@ function mergeReplyFromSource(
599
610
  resolveQuoteIdFromIcqqSource(findIcqqNestedMessageSource(data));
600
611
  if (!quoteId) return content;
601
612
  return [
602
- { type: "reply", data: { id: quoteId, message_id: quoteId } },
613
+ { type: "reply", data: { message_id: quoteId } },
603
614
  ...content.filter((s) => s.type !== "reply"),
604
615
  ];
605
616
  }
@@ -617,7 +628,7 @@ export function resolveInboundContent(
617
628
  } else {
618
629
  content = [{ type: "text", data: { text: "" } }];
619
630
  }
620
- return mergeReplyFromSource(content, data);
631
+ return toCanonicalSegments(mergeReplyFromSource(content, data));
621
632
  }
622
633
 
623
634
  export function normalizeIcqqInboundMessage(