@zhin.js/adapter-qq 6.0.5 → 6.0.7

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @zhin.js/adapter-qq
2
2
 
3
+ ## 6.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 7249733: fix(qq): 群消息 @ 判定改为统一按 mentions 检查,不再按 channelKind 直接视为 @
8
+
9
+ `GROUP_MESSAGE_CREATE` 与 `GROUP_AT_MESSAGE_CREATE` 在 SDK 中都映射为 `message.group`,前者会下发非 @ 的普通群消息;此前 `channelKind === 'group'` 一律置 `mentioned`,导致所有群消息都触发 AI。现统一按 `mentions` 判定:群场景只认 `is_you === true`(精确标识当前机器人;群里 @ 另一个机器人时 `bot: true` 但无 `is_you`,不误判),`bot === true` 回退仅限频道 `AT_MESSAGE_CREATE`(该事件仅 @ 当前机器人时下发,mentions 无 `is_you`)。非 @ 群消息将回到旁听(Passive Group Context)路径。
10
+
11
+ ## 6.0.6
12
+
13
+ ### Patch Changes
14
+
15
+ - d52f3c5: fix: QQ 适配器群聊误识别为私聊、game-kit 启动崩溃、全面补齐 recallMessage
16
+
17
+ - fix(cli): `resolveChannelType` 增加 `metadata.channelKind` 检查,修复 QQ/Discord/KOOK 群聊消息被误分类为私聊
18
+ - fix(game-kit): `createHostGameDb` 改用延迟代理模型,避免数据库启动前解析模型导致崩溃
19
+ - feat(qq): 实现 `recallMessage`,解析复合 messageId 路由到对应 SDK 撤回方法;`normalizeQqMessage` 增加 `message_type` 缺失时的回退检测
20
+ - feat(slack): 实现 `recallMessage`,利用已有 compound ref 和 `chat.delete` API
21
+ - feat(onebot11): 两个 endpoint 类实现 `recallMessage`(`delete_msg`)
22
+ - feat(onebot12): 两个 endpoint 类实现 `recallMessage`(`delete_message`)
23
+ - feat(napcat): 三个 endpoint 类实现 `recallMessage`(`delete_msg`)
24
+ - feat(discord): 两个 endpoint 类实现 `recallMessage`;`send()` 改为返回 `channelId:snowflake` 复合 ID
25
+ - feat(telegram): 实现 `recallMessage`(`deleteMessage`);`send()` 改为返回 `chatId:messageId` 复合 ID
26
+ - feat(kook): 两个 endpoint 类实现 `recallMessage`,利用 kook-client `recallMsg` API
27
+ - feat(lark): 实现 `recallMessage`(`DELETE /im/v1/messages/{id}`)
28
+ - feat(wecom): 实现 `recallMessage`(`POST /cgi-bin/message/recall`)
29
+
3
30
  ## 6.0.5
4
31
 
5
32
  ### Patch Changes
package/lib/endpoint.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare class QqWebsocketEndpoint implements EndpointInstance {
30
30
  readonly target: string;
31
31
  readonly payload: unknown;
32
32
  }): Promise<string>;
33
+ recallMessage(messageId: string): Promise<void>;
33
34
  /** Test / internal: admit a message when open. */
34
35
  admit(msg: QqInboundMessage): void;
35
36
  getGuilds(): Promise<unknown[]>;
@@ -63,6 +64,7 @@ export declare class QqHttpEndpoint implements EndpointInstance {
63
64
  readonly target: string;
64
65
  readonly payload: unknown;
65
66
  }): Promise<string>;
67
+ recallMessage(messageId: string): Promise<void>;
66
68
  admit(msg: QqInboundMessage): void;
67
69
  getGuilds(): Promise<unknown[]>;
68
70
  getChannels(guildId: string): Promise<unknown[]>;
package/lib/endpoint.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { formatCompact, getLogger } from '@zhin.js/logger';
2
2
  import { formatOutbound } from './outbound.js';
3
3
  import { registerQqAgentEndpoint } from './qq-agent-deps.js';
4
- import { formatInboundContent, formatInboundTarget, parseSendTarget, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
4
+ import { formatInboundContent, formatInboundTarget, parseCompoundMessageId, parseSendTarget, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
5
5
  import { defaultCreateHttpBot, registerQqWebhookRoutes, } from './webhook.js';
6
6
  import { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
7
7
  const logger = getLogger('qq');
@@ -99,6 +99,30 @@ export class QqWebsocketEndpoint {
99
99
  }));
100
100
  return messageId;
101
101
  }
102
+ async recallMessage(messageId) {
103
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
104
+ const bot = this.#requireBot();
105
+ switch (kind) {
106
+ case 'private':
107
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
108
+ break;
109
+ case 'group':
110
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
111
+ break;
112
+ case 'channel':
113
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
114
+ break;
115
+ case 'direct':
116
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
117
+ break;
118
+ }
119
+ logger.debug(formatCompact({
120
+ op: 'qq_recall',
121
+ endpoint: this.#options.config.name,
122
+ kind,
123
+ messageId,
124
+ }));
125
+ }
102
126
  /** Test / internal: admit a message when open. */
103
127
  admit(msg) {
104
128
  if (!this.#open)
@@ -255,6 +279,30 @@ export class QqHttpEndpoint {
255
279
  }
256
280
  return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
257
281
  }
282
+ async recallMessage(messageId) {
283
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
284
+ const bot = this.#requireBot();
285
+ switch (kind) {
286
+ case 'private':
287
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
288
+ break;
289
+ case 'group':
290
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
291
+ break;
292
+ case 'channel':
293
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
294
+ break;
295
+ case 'direct':
296
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
297
+ break;
298
+ }
299
+ logger.debug(formatCompact({
300
+ op: 'qq_recall',
301
+ endpoint: this.#options.config.name,
302
+ kind,
303
+ messageId,
304
+ }));
305
+ }
258
306
  admit(msg) {
259
307
  if (!this.#open)
260
308
  return;
package/lib/protocol.d.ts CHANGED
@@ -66,8 +66,10 @@ export interface QqInboundMessage {
66
66
  readonly guildId?: string;
67
67
  readonly rawMessage?: string;
68
68
  /**
69
- * 入站即 @ 机器人(AT_MESSAGE_CREATE 的 mentions[].bot,或群 GROUP_AT_MESSAGE_CREATE
70
- * 事件本身仅 @ 时下发)。新 Runtime 纯文本 content 需经 metadata 传递。
69
+ * 入站是否 @ 当前机器人:统一按 mentions 判定。群场景只认 is_you(@ 另一个
70
+ * 机器人时 bot:true 但无 is_you,不误判);bot 回退仅限频道 AT 事件。
71
+ * 群 GROUP_MESSAGE_CREATE 的非 @ 消息不置位。新 Runtime 纯文本 content 需经
72
+ * metadata 传递。
71
73
  */
72
74
  readonly mentioned?: boolean;
73
75
  }
@@ -92,5 +94,13 @@ export declare function formatInboundContent(msg: QqInboundMessage): string;
92
94
  * Segment canonicalization is intentionally not done here.
93
95
  */
94
96
  export declare function formatOutboundText(payload: unknown): string;
97
+ /**
98
+ * Parse a compound messageId produced by `send()`: `kind-channelId:qqMsgId`.
99
+ */
100
+ export declare function parseCompoundMessageId(messageId: string): {
101
+ kind: QqChannelKind;
102
+ channelId: string;
103
+ qqMsgId: string;
104
+ };
95
105
  /** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
96
106
  export declare function resolveOutboundMessageId(result: unknown): string;
package/lib/protocol.js CHANGED
@@ -120,6 +120,23 @@ export function formatOutboundText(payload) {
120
120
  .filter(Boolean)
121
121
  .join('');
122
122
  }
123
+ /**
124
+ * Parse a compound messageId produced by `send()`: `kind-channelId:qqMsgId`.
125
+ */
126
+ export function parseCompoundMessageId(messageId) {
127
+ const hyphen = messageId.indexOf('-');
128
+ if (hyphen <= 0)
129
+ throw new Error(`invalid QQ messageId format: ${messageId}`);
130
+ const kind = messageId.slice(0, hyphen);
131
+ if (kind !== 'private' && kind !== 'group' && kind !== 'channel' && kind !== 'direct') {
132
+ throw new Error(`invalid QQ messageId kind: ${kind}`);
133
+ }
134
+ const rest = messageId.slice(hyphen + 1);
135
+ const colon = rest.indexOf(':');
136
+ if (colon <= 0)
137
+ throw new Error(`invalid QQ messageId format: ${messageId}`);
138
+ return { kind, channelId: rest.slice(0, colon), qqMsgId: rest.slice(colon + 1) };
139
+ }
123
140
  /** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
124
141
  export function resolveOutboundMessageId(result) {
125
142
  if (!result || typeof result !== 'object') {
package/lib/webhook.js CHANGED
@@ -74,6 +74,10 @@ export function defaultCreateHttpBot(config) {
74
74
  sendGroupMessage: (groupId, message) => bot.sendGroupMessage(groupId, message),
75
75
  sendGuildMessage: (channelId, message) => bot.sendGuildMessage(channelId, message),
76
76
  sendDirectMessage: (guildId, message) => bot.sendDirectMessage(guildId, message),
77
+ recallPrivateMessage: (userId, messageId) => bot.recallPrivateMessage(userId, messageId),
78
+ recallGroupMessage: (groupId, messageId) => bot.recallGroupMessage(groupId, messageId),
79
+ recallGuildMessage: (channelId, messageId) => bot.recallGuildMessage(channelId, messageId),
80
+ recallDirectMessage: (guildId, messageId) => bot.recallDirectMessage(guildId, messageId),
77
81
  getGuilds: () => bot.guildService.getList(),
78
82
  getChannels: (guildId) => bot.channelService.getList(guildId),
79
83
  getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
package/lib/ws.d.ts CHANGED
@@ -10,6 +10,10 @@ export interface QqBotTransport {
10
10
  sendGroupMessage(groupId: string, message: QqOutboundMessage): Promise<unknown>;
11
11
  sendGuildMessage(channelId: string, message: QqOutboundMessage): Promise<unknown>;
12
12
  sendDirectMessage?(guildId: string, message: QqOutboundMessage): Promise<unknown>;
13
+ recallPrivateMessage?(userId: string, messageId: string): Promise<unknown>;
14
+ recallGroupMessage?(groupId: string, messageId: string): Promise<unknown>;
15
+ recallGuildMessage?(channelId: string, messageId: string): Promise<unknown>;
16
+ recallDirectMessage?(guildId: string, messageId: string): Promise<unknown>;
13
17
  getGuilds(): Promise<unknown[]>;
14
18
  getChannels(guildId: string): Promise<unknown[]>;
15
19
  getChannelInfo(channelId: string): Promise<unknown>;
package/lib/ws.js CHANGED
@@ -34,13 +34,17 @@ export function normalizeQqMessage(raw) {
34
34
  return null;
35
35
  let channelKind = 'private';
36
36
  let channelId = '';
37
- if (msg.message_type === 'guild') {
37
+ const isGroup = msg.message_type === 'group'
38
+ || (msg.message_type == null && (msg.group_id != null || msg.group_openid != null));
39
+ const isGuild = msg.message_type === 'guild'
40
+ || (msg.message_type == null && !isGroup && msg.guild_id != null && msg.channel_id != null);
41
+ if (isGuild) {
38
42
  channelKind = msg.sub_type === 'direct' ? 'direct' : 'channel';
39
43
  channelId = channelKind === 'direct'
40
44
  ? String(msg.guild_id ?? '')
41
45
  : String(msg.channel_id ?? '');
42
46
  }
43
- else if (msg.message_type === 'group') {
47
+ else if (isGroup) {
44
48
  channelKind = 'group';
45
49
  channelId = String(msg.group_id ?? msg.group_openid ?? '');
46
50
  }
@@ -59,9 +63,12 @@ export function normalizeQqMessage(raw) {
59
63
  const content = extractTextContent(msg.message) || msg.raw_message || '';
60
64
  const rawRoles = msg.member?.roles ?? msg.sender?.roles;
61
65
  const authorRoles = Array.isArray(rawRoles) ? rawRoles.map(String) : undefined;
62
- // 群 GROUP_AT_MESSAGE_CREATE 仅 @ 机器人时下发;频道 AT 事件看 mentions[].bot
63
- const mentioned = channelKind === 'group'
64
- || (Array.isArray(msg.mentions) && msg.mentions.some((m) => m?.bot === true));
66
+ // 统一按 mentions 判定 @:is_you 精确标识当前机器人(群载荷实测字段)。
67
+ // 群里 @ 另一个机器人时 mentions 带 bot:true 但无 is_you,不能误判为 @ 当前机器人,
68
+ // 故群场景只认 is_you;bot 回退仅限频道 AT_MESSAGE_CREATE(该事件仅 @ 当前
69
+ // 机器人时下发,其 mentions 无 is_you)。
70
+ const mentioned = Array.isArray(msg.mentions)
71
+ && msg.mentions.some((m) => m?.is_you === true || (m?.bot === true && channelKind !== 'group'));
65
72
  return {
66
73
  id: String(msg.message_id),
67
74
  content,
@@ -117,6 +124,10 @@ export function defaultCreateBot(config) {
117
124
  sendGroupMessage: (groupId, message) => bot.sendGroupMessage(groupId, message),
118
125
  sendGuildMessage: (channelId, message) => bot.sendGuildMessage(channelId, message),
119
126
  sendDirectMessage: (guildId, message) => bot.sendDirectMessage(guildId, message),
127
+ recallPrivateMessage: (userId, messageId) => bot.recallPrivateMessage(userId, messageId),
128
+ recallGroupMessage: (groupId, messageId) => bot.recallGroupMessage(groupId, messageId),
129
+ recallGuildMessage: (channelId, messageId) => bot.recallGuildMessage(channelId, messageId),
130
+ recallDirectMessage: (guildId, messageId) => bot.recallDirectMessage(guildId, messageId),
120
131
  getGuilds: () => bot.guildService.getList(),
121
132
  getChannels: (guildId) => bot.channelService.getList(guildId),
122
133
  getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-qq",
3
- "version": "6.0.5",
3
+ "version": "6.0.7",
4
4
  "description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -37,9 +37,9 @@
37
37
  "qq-official-bot": "^1.2.3",
38
38
  "@zhin.js/adapter": "1.1.2",
39
39
  "@zhin.js/command": "1.0.4",
40
+ "@zhin.js/logger": "1.0.75",
40
41
  "@zhin.js/core": "1.4.3",
41
42
  "@zhin.js/host-http": "1.0.4",
42
- "@zhin.js/logger": "1.0.75",
43
43
  "@zhin.js/plugin-runtime": "1.1.1"
44
44
  },
45
45
  "peerDependencies": {
@@ -47,8 +47,8 @@
47
47
  "@zhin.js/adapter": "1.1.2",
48
48
  "@zhin.js/agent": "1.0.10",
49
49
  "@zhin.js/core": "1.4.3",
50
- "@zhin.js/plugin-runtime": "1.1.1",
51
- "zhin.js": "5.0.3"
50
+ "zhin.js": "5.0.3",
51
+ "@zhin.js/plugin-runtime": "1.1.1"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "zhin.js": {
package/src/endpoint.ts CHANGED
@@ -15,6 +15,7 @@ import { registerQqAgentEndpoint } from './qq-agent-deps.js';
15
15
  import {
16
16
  formatInboundContent,
17
17
  formatInboundTarget,
18
+ parseCompoundMessageId,
18
19
  parseSendTarget,
19
20
  resolveOutboundMessageId,
20
21
  senderDisplayName,
@@ -149,6 +150,31 @@ export class QqWebsocketEndpoint implements EndpointInstance {
149
150
  return messageId;
150
151
  }
151
152
 
153
+ async recallMessage(messageId: string): Promise<void> {
154
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
155
+ const bot = this.#requireBot();
156
+ switch (kind) {
157
+ case 'private':
158
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
159
+ break;
160
+ case 'group':
161
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
162
+ break;
163
+ case 'channel':
164
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
165
+ break;
166
+ case 'direct':
167
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
168
+ break;
169
+ }
170
+ logger.debug(formatCompact({
171
+ op: 'qq_recall',
172
+ endpoint: this.#options.config.name,
173
+ kind,
174
+ messageId,
175
+ }));
176
+ }
177
+
152
178
  /** Test / internal: admit a message when open. */
153
179
  admit(msg: QqInboundMessage): void {
154
180
  if (!this.#open) return;
@@ -329,6 +355,31 @@ export class QqHttpEndpoint implements EndpointInstance {
329
355
  return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
330
356
  }
331
357
 
358
+ async recallMessage(messageId: string): Promise<void> {
359
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
360
+ const bot = this.#requireBot();
361
+ switch (kind) {
362
+ case 'private':
363
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
364
+ break;
365
+ case 'group':
366
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
367
+ break;
368
+ case 'channel':
369
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
370
+ break;
371
+ case 'direct':
372
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
373
+ break;
374
+ }
375
+ logger.debug(formatCompact({
376
+ op: 'qq_recall',
377
+ endpoint: this.#options.config.name,
378
+ kind,
379
+ messageId,
380
+ }));
381
+ }
382
+
332
383
  admit(msg: QqInboundMessage): void {
333
384
  if (!this.#open) return;
334
385
  const target = formatInboundTarget(msg);
package/src/protocol.ts CHANGED
@@ -77,8 +77,10 @@ export interface QqInboundMessage {
77
77
  readonly guildId?: string;
78
78
  readonly rawMessage?: string;
79
79
  /**
80
- * 入站即 @ 机器人(AT_MESSAGE_CREATE 的 mentions[].bot,或群 GROUP_AT_MESSAGE_CREATE
81
- * 事件本身仅 @ 时下发)。新 Runtime 纯文本 content 需经 metadata 传递。
80
+ * 入站是否 @ 当前机器人:统一按 mentions 判定。群场景只认 is_you(@ 另一个
81
+ * 机器人时 bot:true 但无 is_you,不误判);bot 回退仅限频道 AT 事件。
82
+ * 群 GROUP_MESSAGE_CREATE 的非 @ 消息不置位。新 Runtime 纯文本 content 需经
83
+ * metadata 传递。
82
84
  */
83
85
  readonly mentioned?: boolean;
84
86
  }
@@ -219,6 +221,26 @@ export function formatOutboundText(payload: unknown): string {
219
221
  .join('');
220
222
  }
221
223
 
224
+ /**
225
+ * Parse a compound messageId produced by `send()`: `kind-channelId:qqMsgId`.
226
+ */
227
+ export function parseCompoundMessageId(messageId: string): {
228
+ kind: QqChannelKind;
229
+ channelId: string;
230
+ qqMsgId: string;
231
+ } {
232
+ const hyphen = messageId.indexOf('-');
233
+ if (hyphen <= 0) throw new Error(`invalid QQ messageId format: ${messageId}`);
234
+ const kind = messageId.slice(0, hyphen);
235
+ if (kind !== 'private' && kind !== 'group' && kind !== 'channel' && kind !== 'direct') {
236
+ throw new Error(`invalid QQ messageId kind: ${kind}`);
237
+ }
238
+ const rest = messageId.slice(hyphen + 1);
239
+ const colon = rest.indexOf(':');
240
+ if (colon <= 0) throw new Error(`invalid QQ messageId format: ${messageId}`);
241
+ return { kind, channelId: rest.slice(0, colon), qqMsgId: rest.slice(colon + 1) };
242
+ }
243
+
222
244
  /** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
223
245
  export function resolveOutboundMessageId(result: unknown): string {
224
246
  if (!result || typeof result !== 'object') {
package/src/webhook.ts CHANGED
@@ -101,6 +101,10 @@ export function defaultCreateHttpBot(config: ResolvedQqHttpConfig): QqHttpBotTra
101
101
  sendGroupMessage: (groupId, message) => bot.sendGroupMessage(groupId, message as Sendable),
102
102
  sendGuildMessage: (channelId, message) => bot.sendGuildMessage(channelId, message as Sendable),
103
103
  sendDirectMessage: (guildId, message) => bot.sendDirectMessage(guildId, message as Sendable),
104
+ recallPrivateMessage: (userId, messageId) => bot.recallPrivateMessage(userId, messageId),
105
+ recallGroupMessage: (groupId, messageId) => bot.recallGroupMessage(groupId, messageId),
106
+ recallGuildMessage: (channelId, messageId) => bot.recallGuildMessage(channelId, messageId),
107
+ recallDirectMessage: (guildId, messageId) => bot.recallDirectMessage(guildId, messageId),
104
108
  getGuilds: () => bot.guildService.getList(),
105
109
  getChannels: (guildId) => bot.channelService.getList(guildId),
106
110
  getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),
package/src/ws.ts CHANGED
@@ -17,6 +17,10 @@ export interface QqBotTransport {
17
17
  sendGroupMessage(groupId: string, message: QqOutboundMessage): Promise<unknown>;
18
18
  sendGuildMessage(channelId: string, message: QqOutboundMessage): Promise<unknown>;
19
19
  sendDirectMessage?(guildId: string, message: QqOutboundMessage): Promise<unknown>;
20
+ recallPrivateMessage?(userId: string, messageId: string): Promise<unknown>;
21
+ recallGroupMessage?(groupId: string, messageId: string): Promise<unknown>;
22
+ recallGuildMessage?(channelId: string, messageId: string): Promise<unknown>;
23
+ recallDirectMessage?(guildId: string, messageId: string): Promise<unknown>;
20
24
  getGuilds(): Promise<unknown[]>;
21
25
  getChannels(guildId: string): Promise<unknown[]>;
22
26
  getChannelInfo(channelId: string): Promise<unknown>;
@@ -90,19 +94,25 @@ export function normalizeQqMessage(raw: unknown): QqInboundMessage | null {
90
94
  sender?: { user_id?: string | number; user_name?: string; roles?: (string | number)[] };
91
95
  member?: { roles?: (string | number)[] };
92
96
  author?: { member_openid?: string; user_openid?: string; id?: string; username?: string };
93
- /** AT_MESSAGE_CREATE 载荷:被 @ 的用户列表,bot=true 即机器人 */
94
- mentions?: Array<{ id?: string | number; bot?: boolean }>;
97
+ /** AT 事件载荷:被 @ 的用户列表;is_you 标识当前机器人,bot 标识机器人账号 */
98
+ mentions?: Array<{ id?: string | number; bot?: boolean; is_you?: boolean; member_openid?: string; member_role?: string }>;
95
99
  };
96
100
  if (msg.message_id == null) return null;
97
101
 
98
102
  let channelKind: QqChannelKind = 'private';
99
103
  let channelId = '';
100
- if (msg.message_type === 'guild') {
104
+
105
+ const isGroup = msg.message_type === 'group'
106
+ || (msg.message_type == null && (msg.group_id != null || msg.group_openid != null));
107
+ const isGuild = msg.message_type === 'guild'
108
+ || (msg.message_type == null && !isGroup && msg.guild_id != null && msg.channel_id != null);
109
+
110
+ if (isGuild) {
101
111
  channelKind = msg.sub_type === 'direct' ? 'direct' : 'channel';
102
112
  channelId = channelKind === 'direct'
103
113
  ? String(msg.guild_id ?? '')
104
114
  : String(msg.channel_id ?? '');
105
- } else if (msg.message_type === 'group') {
115
+ } else if (isGroup) {
106
116
  channelKind = 'group';
107
117
  channelId = String(msg.group_id ?? msg.group_openid ?? '');
108
118
  } else {
@@ -122,9 +132,12 @@ export function normalizeQqMessage(raw: unknown): QqInboundMessage | null {
122
132
  const content = extractTextContent(msg.message) || msg.raw_message || '';
123
133
  const rawRoles = msg.member?.roles ?? msg.sender?.roles;
124
134
  const authorRoles = Array.isArray(rawRoles) ? rawRoles.map(String) : undefined;
125
- // 群 GROUP_AT_MESSAGE_CREATE 仅 @ 机器人时下发;频道 AT 事件看 mentions[].bot
126
- const mentioned = channelKind === 'group'
127
- || (Array.isArray(msg.mentions) && msg.mentions.some((m) => m?.bot === true));
135
+ // 统一按 mentions 判定 @:is_you 精确标识当前机器人(群载荷实测字段)。
136
+ // 群里 @ 另一个机器人时 mentions 带 bot:true 但无 is_you,不能误判为 @ 当前机器人,
137
+ // 故群场景只认 is_you;bot 回退仅限频道 AT_MESSAGE_CREATE(该事件仅 @ 当前
138
+ // 机器人时下发,其 mentions 无 is_you)。
139
+ const mentioned = Array.isArray(msg.mentions)
140
+ && msg.mentions.some((m) => m?.is_you === true || (m?.bot === true && channelKind !== 'group'));
128
141
 
129
142
  return {
130
143
  id: String(msg.message_id),
@@ -180,6 +193,10 @@ export function defaultCreateBot(config: ResolvedQqWebsocketConfig): QqBotTransp
180
193
  sendGroupMessage: (groupId, message) => bot.sendGroupMessage(groupId, message as Sendable),
181
194
  sendGuildMessage: (channelId, message) => bot.sendGuildMessage(channelId, message as Sendable),
182
195
  sendDirectMessage: (guildId, message) => bot.sendDirectMessage(guildId, message as Sendable),
196
+ recallPrivateMessage: (userId, messageId) => bot.recallPrivateMessage(userId, messageId),
197
+ recallGroupMessage: (groupId, messageId) => bot.recallGroupMessage(groupId, messageId),
198
+ recallGuildMessage: (channelId, messageId) => bot.recallGuildMessage(channelId, messageId),
199
+ recallDirectMessage: (guildId, messageId) => bot.recallDirectMessage(guildId, messageId),
183
200
  getGuilds: () => bot.guildService.getList(),
184
201
  getChannels: (guildId) => bot.channelService.getList(guildId),
185
202
  getChannelInfo: (channelId) => bot.channelService.getInfo(channelId),