@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 +27 -0
- package/lib/endpoint.d.ts +2 -0
- package/lib/endpoint.js +49 -1
- package/lib/protocol.d.ts +12 -2
- package/lib/protocol.js +17 -0
- package/lib/webhook.js +4 -0
- package/lib/ws.d.ts +4 -0
- package/lib/ws.js +16 -5
- package/package.json +4 -4
- package/src/endpoint.ts +51 -0
- package/src/protocol.ts +24 -2
- package/src/webhook.ts +4 -0
- package/src/ws.ts +24 -7
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
|
-
*
|
|
70
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
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
|
-
"
|
|
51
|
-
"zhin.js": "
|
|
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
|
-
*
|
|
81
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
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),
|