@ryantest/openclaw-qqbot 1.6.6-alpha.4 → 1.6.7-beta.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.
- package/README.md +24 -15
- package/README.zh.md +24 -15
- package/dist/src/api.d.ts +32 -5
- package/dist/src/api.js +111 -12
- package/dist/src/channel.d.ts +18 -0
- package/dist/src/channel.js +85 -2
- package/dist/src/config.d.ts +33 -2
- package/dist/src/config.js +125 -1
- package/dist/src/gateway.js +566 -24
- package/dist/src/group-history.d.ts +136 -0
- package/dist/src/group-history.js +226 -0
- package/dist/src/message-gating.d.ts +53 -0
- package/dist/src/message-gating.js +107 -0
- package/dist/src/message-queue.d.ts +36 -0
- package/dist/src/message-queue.js +164 -22
- package/dist/src/outbound.d.ts +4 -4
- package/dist/src/outbound.js +18 -6
- package/dist/src/ref-index-store.js +5 -28
- package/dist/src/request-context.d.ts +7 -0
- package/dist/src/request-context.js +7 -0
- package/dist/src/slash-commands.d.ts +6 -0
- package/dist/src/slash-commands.js +3 -3
- package/dist/src/tools/remind.js +17 -9
- package/dist/src/types.d.ts +90 -2
- package/dist/src/utils/audio-convert.d.ts +1 -1
- package/dist/src/utils/audio-convert.js +1 -1
- package/dist/src/utils/chunked-upload.d.ts +11 -2
- package/dist/src/utils/chunked-upload.js +63 -11
- package/dist/src/utils/media-send.js +1 -1
- package/dist/src/utils/text-parsing.js +7 -18
- package/package.json +1 -1
- package/scripts/postinstall-link-sdk.js +22 -9
- package/scripts/upgrade-via-npm.sh +11 -3
- package/scripts/upgrade-via-source.sh +63 -15
- package/skills/qqbot-remind/SKILL.md +21 -11
- package/src/api.ts +135 -7
- package/src/channel.ts +85 -2
- package/src/config.ts +170 -3
- package/src/gateway.ts +662 -29
- package/src/group-history.ts +328 -0
- package/src/message-gating.ts +190 -0
- package/src/message-queue.ts +201 -21
- package/src/openclaw-plugin-sdk.d.ts +65 -0
- package/src/outbound.ts +18 -6
- package/src/ref-index-store.ts +5 -27
- package/src/request-context.ts +10 -0
- package/src/slash-commands.ts +3 -3
- package/src/tools/remind.ts +17 -9
- package/src/types.ts +94 -2
- package/src/utils/audio-convert.ts +1 -1
- package/src/utils/chunked-upload.ts +76 -12
- package/src/utils/media-send.ts +1 -2
- package/src/utils/text-parsing.ts +7 -14
package/dist/src/gateway.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, initApiConfig, startBackgroundTokenRefresh, stopBackgroundTokenRefresh, sendC2CInputNotify, onMessageSent, PLUGIN_USER_AGENT, acknowledgeInteraction, getApiPluginVersion } from "./api.js";
|
|
4
5
|
import { loadSession, saveSession, clearSession } from "./session-store.js";
|
|
5
6
|
import { recordKnownUser, flushKnownUsers } from "./known-users.js";
|
|
6
7
|
import { getQQBotRuntime } from "./runtime.js";
|
|
8
|
+
import { isGroupAllowed, resolveGroupName, resolveGroupPrompt, resolveHistoryLimit, resolveGroupPolicy, resolveGroupConfig, resolveIgnoreOtherMentions, resolveMentionPatterns } from "./config.js";
|
|
9
|
+
import { qqbotPlugin, stripMentionText, detectWasMentioned } from "./channel.js";
|
|
10
|
+
import { recordPendingHistoryEntry, buildPendingHistoryContext, buildMergedMessageContext, clearPendingHistory, formatAttachmentTags, formatMessageContent, toAttachmentSummaries, } from "./group-history.js";
|
|
7
11
|
import { setRefIndex, getRefIndex, formatRefEntryForAgent, flushRefIndex } from "./ref-index-store.js";
|
|
8
|
-
import { matchSlashCommand } from "./slash-commands.js";
|
|
12
|
+
import { matchSlashCommand, getFrameworkVersion, parseFrameworkDateVersion } from "./slash-commands.js";
|
|
9
13
|
import { createMessageQueue } from "./message-queue.js";
|
|
10
14
|
import { triggerUpdateCheck } from "./update-checker.js";
|
|
11
15
|
import { startImageServer, isImageServerRunning } from "./image-server.js";
|
|
@@ -21,6 +25,232 @@ import { parseAndSendMediaTags, sendPlainReply } from "./outbound-deliver.js";
|
|
|
21
25
|
import { createDeliverDebouncer } from "./deliver-debounce.js";
|
|
22
26
|
import { runWithRequestContext } from "./request-context.js";
|
|
23
27
|
import { StreamingController, shouldUseStreaming } from "./streaming.js";
|
|
28
|
+
import { resolveGroupMessageGate } from "./message-gating.js";
|
|
29
|
+
// ============ Interaction 处理 ============
|
|
30
|
+
/** 配置查询交互类型 */
|
|
31
|
+
const INTERACTION_TYPE_CONFIG_QUERY = 2001;
|
|
32
|
+
/** 配置更新交互类型 */
|
|
33
|
+
const INTERACTION_TYPE_CONFIG_UPDATE = 2002;
|
|
34
|
+
/** 处理 INTERACTION_CREATE 事件 */
|
|
35
|
+
async function handleInteractionCreate(params) {
|
|
36
|
+
const { event, account, cfg, log } = params;
|
|
37
|
+
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
38
|
+
if (event.data?.type === INTERACTION_TYPE_CONFIG_QUERY) {
|
|
39
|
+
// 从框架 configApi 读取最新配置(而非闭包中的旧 cfg),确保配置查询返回的数据与磁盘一致
|
|
40
|
+
const runtime = getQQBotRuntime();
|
|
41
|
+
const configApi = runtime.config;
|
|
42
|
+
const latestCfg = configApi.loadConfig();
|
|
43
|
+
const groupOpenid = event.group_openid ?? "";
|
|
44
|
+
const groupCfg = groupOpenid ? resolveGroupConfig(latestCfg, groupOpenid, account.accountId) : null;
|
|
45
|
+
const groupPolicy = resolveGroupPolicy(latestCfg, account.accountId);
|
|
46
|
+
// require_mention 协议:字符串 "mention" | "always"(mention=@机器人时激活,always=总是激活)
|
|
47
|
+
const configRequireMention = groupCfg?.requireMention ?? true;
|
|
48
|
+
const requireMentionMode = configRequireMention ? "mention" : "always";
|
|
49
|
+
const pluginVersion = getApiPluginVersion();
|
|
50
|
+
const fwVersionRaw = getFrameworkVersion();
|
|
51
|
+
const clawVer = parseFrameworkDateVersion(fwVersionRaw) ?? fwVersionRaw;
|
|
52
|
+
// 通过路由解析 agentId(与消息处理流程一致),用于 agent-aware 的 mentionPatterns
|
|
53
|
+
const interactionAgentId = groupOpenid
|
|
54
|
+
? runtime.channel?.routing?.resolveAgentRoute?.({
|
|
55
|
+
cfg: latestCfg,
|
|
56
|
+
channel: "qqbot",
|
|
57
|
+
accountId: account.accountId,
|
|
58
|
+
peer: { kind: "group", id: groupOpenid },
|
|
59
|
+
})?.agentId
|
|
60
|
+
: undefined;
|
|
61
|
+
// mention_patterns 协议:逗号分隔的字符串(@文本的名称提及BOT名,多个使用,分隔)
|
|
62
|
+
const mentionPatternsArr = resolveMentionPatterns(latestCfg, interactionAgentId);
|
|
63
|
+
const mentionPatterns = mentionPatternsArr.join(",");
|
|
64
|
+
const clawCfg = {
|
|
65
|
+
channel_type: "qqbot",
|
|
66
|
+
channel_ver: pluginVersion,
|
|
67
|
+
claw_type: "openclaw",
|
|
68
|
+
claw_ver: clawVer,
|
|
69
|
+
require_mention: requireMentionMode,
|
|
70
|
+
group_policy: groupPolicy,
|
|
71
|
+
mention_patterns: mentionPatterns,
|
|
72
|
+
online_state: "online",
|
|
73
|
+
};
|
|
74
|
+
await acknowledgeInteraction(token, event.id, 0, { claw_cfg: clawCfg });
|
|
75
|
+
log?.info(`[qqbot:${account.accountId}] Interaction ACK (type=${INTERACTION_TYPE_CONFIG_QUERY}) sent: ${event.id}, claw_cfg=${JSON.stringify(clawCfg)}`);
|
|
76
|
+
}
|
|
77
|
+
else if (event.data?.type === INTERACTION_TYPE_CONFIG_UPDATE) {
|
|
78
|
+
// type=2002: 配置更新交互,从 resolved.claw_cfg 获取更新信息并写入本地配置
|
|
79
|
+
const resolved = event.data.resolved;
|
|
80
|
+
const clawCfgUpdate = resolved?.claw_cfg;
|
|
81
|
+
const groupOpenid = event.group_openid ?? "";
|
|
82
|
+
const runtime = getQQBotRuntime();
|
|
83
|
+
const configApi = runtime.config;
|
|
84
|
+
const currentCfg = structuredClone(configApi.loadConfig());
|
|
85
|
+
const qqbot = (currentCfg.channels ?? {}).qqbot;
|
|
86
|
+
let changed = false;
|
|
87
|
+
if (clawCfgUpdate) {
|
|
88
|
+
// 更新 require_mention(群级别)——协议为 "mention" | "always",写回配置时转为 boolean
|
|
89
|
+
if (clawCfgUpdate.require_mention !== undefined && groupOpenid && qqbot) {
|
|
90
|
+
const requireMentionBool = clawCfgUpdate.require_mention === "mention";
|
|
91
|
+
const accountId = account.accountId;
|
|
92
|
+
const isNamedAccount = accountId !== "default" && qqbot.accounts?.[accountId];
|
|
93
|
+
if (isNamedAccount) {
|
|
94
|
+
const accounts = qqbot.accounts;
|
|
95
|
+
const acct = accounts[accountId] ?? {};
|
|
96
|
+
const groups = (acct.groups ?? {});
|
|
97
|
+
groups[groupOpenid] = { ...groups[groupOpenid], requireMention: requireMentionBool };
|
|
98
|
+
acct.groups = groups;
|
|
99
|
+
accounts[accountId] = acct;
|
|
100
|
+
qqbot.accounts = accounts;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const groups = (qqbot.groups ?? {});
|
|
104
|
+
groups[groupOpenid] = { ...groups[groupOpenid], requireMention: requireMentionBool };
|
|
105
|
+
qqbot.groups = groups;
|
|
106
|
+
}
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (changed) {
|
|
111
|
+
await configApi.writeConfigFile(currentCfg);
|
|
112
|
+
log?.info(`[qqbot:${account.accountId}] Config updated via interaction ${event.id}: ${JSON.stringify({
|
|
113
|
+
require_mention: clawCfgUpdate?.require_mention,
|
|
114
|
+
group_openid: groupOpenid || undefined,
|
|
115
|
+
})}`);
|
|
116
|
+
}
|
|
117
|
+
// 无论更新是否成功,ACK 都上报最新的 claw_cfg 快照(写入后重新读取确保一致)
|
|
118
|
+
const latestCfg = changed ? configApi.loadConfig() : currentCfg;
|
|
119
|
+
const updatedGroupCfg = groupOpenid ? resolveGroupConfig(latestCfg, groupOpenid, account.accountId) : null;
|
|
120
|
+
const updatedRequireMention = updatedGroupCfg?.requireMention ?? true;
|
|
121
|
+
const updatedRequireMentionMode = updatedRequireMention ? "mention" : "always";
|
|
122
|
+
const pluginVersion = getApiPluginVersion();
|
|
123
|
+
const fwVersionRaw = getFrameworkVersion();
|
|
124
|
+
const clawVer = parseFrameworkDateVersion(fwVersionRaw) ?? fwVersionRaw;
|
|
125
|
+
const ackClawCfg = {
|
|
126
|
+
channel_type: "qqbot",
|
|
127
|
+
channel_ver: pluginVersion,
|
|
128
|
+
claw_type: "openclaw",
|
|
129
|
+
claw_ver: clawVer,
|
|
130
|
+
require_mention: updatedRequireMentionMode,
|
|
131
|
+
online_state: "online",
|
|
132
|
+
};
|
|
133
|
+
await acknowledgeInteraction(token, event.id, 0, { claw_cfg: ackClawCfg });
|
|
134
|
+
log?.info(`[qqbot:${account.accountId}] Interaction ACK (type=${INTERACTION_TYPE_CONFIG_UPDATE}) sent: ${event.id}, claw_cfg=${JSON.stringify(ackClawCfg)}`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// 其他类型:普通 ACK
|
|
138
|
+
await acknowledgeInteraction(token, event.id);
|
|
139
|
+
log?.debug?.(`[qqbot:${account.accountId}] Interaction ACK sent: ${event.id}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 解析 session store 文件路径 */
|
|
143
|
+
function resolveSessionStorePath(cfg, agentId) {
|
|
144
|
+
const sessionCfg = cfg?.session;
|
|
145
|
+
const store = sessionCfg?.store;
|
|
146
|
+
const resolvedAgentId = agentId || "default";
|
|
147
|
+
if (store) {
|
|
148
|
+
let expanded = store;
|
|
149
|
+
if (expanded.includes("{agentId}")) {
|
|
150
|
+
expanded = expanded.replaceAll("{agentId}", resolvedAgentId);
|
|
151
|
+
}
|
|
152
|
+
if (expanded.startsWith("~")) {
|
|
153
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
154
|
+
expanded = expanded.replace(/^~/, home);
|
|
155
|
+
}
|
|
156
|
+
return path.resolve(expanded);
|
|
157
|
+
}
|
|
158
|
+
// 默认路径: ~/.openclaw/agents/{agentId}/sessions/sessions.json
|
|
159
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim()
|
|
160
|
+
|| process.env.CLAWDBOT_STATE_DIR?.trim()
|
|
161
|
+
|| path.join(process.env.HOME || process.env.USERPROFILE || "", ".openclaw");
|
|
162
|
+
return path.join(stateDir, "agents", resolvedAgentId, "sessions", "sessions.json");
|
|
163
|
+
}
|
|
164
|
+
// ============ Mention Gating — 已抽取到 message-gating.ts ============
|
|
165
|
+
// ============ Command Detection(委托框架运行时 commands-registry) ============
|
|
166
|
+
/**
|
|
167
|
+
* 检测消息是否包含框架控制命令(如 /activation、/status 等)。
|
|
168
|
+
*
|
|
169
|
+
* 不再使用静态 KNOWN_CONTROL_COMMANDS 列表,而是委托给框架运行时
|
|
170
|
+
* pluginRuntime.channel.text.hasControlCommand(),确保框架新增命令时
|
|
171
|
+
* 无需手动同步。
|
|
172
|
+
*
|
|
173
|
+
* 如果 pluginRuntime 尚未初始化(极端边界),回退到简单的 "/" 前缀检测。
|
|
174
|
+
*/
|
|
175
|
+
function hasControlCommand(text) {
|
|
176
|
+
if (!text || !text.startsWith("/"))
|
|
177
|
+
return false;
|
|
178
|
+
try {
|
|
179
|
+
const runtime = getQQBotRuntime();
|
|
180
|
+
const runtimeHasControlCommand = runtime?.channel?.text?.hasControlCommand;
|
|
181
|
+
if (typeof runtimeHasControlCommand === "function") {
|
|
182
|
+
return runtimeHasControlCommand(text);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// runtime 未初始化,fallback
|
|
187
|
+
}
|
|
188
|
+
// fallback:简单的 "/" + word 检测(宁可误判为 true 也不漏掉命令)
|
|
189
|
+
return /^\/[a-z][a-z0-9_-]*/i.test(text);
|
|
190
|
+
}
|
|
191
|
+
// ============ Text Command Gating ============
|
|
192
|
+
/**
|
|
193
|
+
* 判断文本命令是否启用。
|
|
194
|
+
* 当 cfg.commands.text === false 时禁用;QQ Bot 仅支持文本命令(无 native slash command)。
|
|
195
|
+
*/
|
|
196
|
+
function shouldHandleTextCommands(cfg) {
|
|
197
|
+
const commands = cfg.commands;
|
|
198
|
+
// 仅当显式设置为 false 时禁用(默认启用)
|
|
199
|
+
return commands?.text !== false;
|
|
200
|
+
}
|
|
201
|
+
// ============ hasAnyMention 检测 ============
|
|
202
|
+
/**
|
|
203
|
+
* 检测消息中是否包含任何 @mention(不限于 @bot)。
|
|
204
|
+
* 如果消息 @ 了任何人,即使是控制命令也不应该 bypass mention 门控。
|
|
205
|
+
*/
|
|
206
|
+
function hasAnyMention(params) {
|
|
207
|
+
// QQ 事件中 mentions 数组包含了消息中所有被 @ 的用户(含 bot)
|
|
208
|
+
if (params.mentions && params.mentions.length > 0)
|
|
209
|
+
return true;
|
|
210
|
+
// 兜底:检查文本中是否有 <@xxx> 格式的 mention
|
|
211
|
+
if (params.content && /<@!?\w+>/.test(params.content))
|
|
212
|
+
return true;
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
// ============ implicitMention 检测 ============
|
|
216
|
+
/**
|
|
217
|
+
* 检测引用回复是否构成隐式 mention。
|
|
218
|
+
* 如果用户回复的是 bot 发出的消息,视为隐式 mention。
|
|
219
|
+
*/
|
|
220
|
+
function resolveImplicitMention(params) {
|
|
221
|
+
if (!params.refMsgIdx)
|
|
222
|
+
return false;
|
|
223
|
+
const refEntry = params.getRefEntry(params.refMsgIdx);
|
|
224
|
+
return refEntry?.isBot === true;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 解析 groupActivation(session store > 配置 requireMention > 默认值)
|
|
228
|
+
* @returns "mention" | "always"
|
|
229
|
+
*/
|
|
230
|
+
function resolveGroupActivation(params) {
|
|
231
|
+
const defaultActivation = params.configRequireMention ? "mention" : "always";
|
|
232
|
+
try {
|
|
233
|
+
const storePath = resolveSessionStorePath(params.cfg, params.agentId);
|
|
234
|
+
if (!fs.existsSync(storePath)) {
|
|
235
|
+
return defaultActivation;
|
|
236
|
+
}
|
|
237
|
+
const raw = fs.readFileSync(storePath, "utf-8");
|
|
238
|
+
const store = JSON.parse(raw);
|
|
239
|
+
const entry = store[params.sessionKey];
|
|
240
|
+
if (!entry?.groupActivation) {
|
|
241
|
+
return defaultActivation;
|
|
242
|
+
}
|
|
243
|
+
const normalized = entry.groupActivation.trim().toLowerCase();
|
|
244
|
+
if (normalized === "mention" || normalized === "always") {
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
return defaultActivation;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// session store 读取失败时 fallback 到配置文件
|
|
251
|
+
return defaultActivation;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
24
254
|
// QQ Bot intents - 按权限级别分组
|
|
25
255
|
const INTENTS = {
|
|
26
256
|
// 基础权限(默认有)
|
|
@@ -30,10 +260,11 @@ const INTENTS = {
|
|
|
30
260
|
// 需要申请的权限
|
|
31
261
|
DIRECT_MESSAGE: 1 << 12, // 频道私信
|
|
32
262
|
GROUP_AND_C2C: 1 << 25, // 群聊和 C2C 私聊(需申请)
|
|
263
|
+
INTERACTION: 1 << 26, // 按钮交互回调
|
|
33
264
|
};
|
|
34
|
-
// 固定使用完整权限(群聊 + 私信 +
|
|
35
|
-
const FULL_INTENTS = INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C;
|
|
36
|
-
const FULL_INTENTS_DESC = "
|
|
265
|
+
// 固定使用完整权限(群聊 + 私信 + 频道 + 交互),不做降级
|
|
266
|
+
const FULL_INTENTS = INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C | INTENTS.INTERACTION;
|
|
267
|
+
const FULL_INTENTS_DESC = "群聊+私信+频道+交互";
|
|
37
268
|
// 重连配置
|
|
38
269
|
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000, 60000]; // 递增延迟
|
|
39
270
|
const RATE_LIMIT_DELAY = 60000; // 遇到频率限制时等待 60 秒
|
|
@@ -189,7 +420,7 @@ export async function startGateway(ctx) {
|
|
|
189
420
|
lastSeq = savedSession.lastSeq;
|
|
190
421
|
log?.info(`[qqbot:${account.accountId}] Restored session from storage: sessionId=${sessionId}, lastSeq=${lastSeq}`);
|
|
191
422
|
}
|
|
192
|
-
// ============
|
|
423
|
+
// ============ 消息队列(复用 createMessageQueue,内置群消息合并/淘汰策略) ============
|
|
193
424
|
const msgQueue = createMessageQueue({
|
|
194
425
|
accountId: account.accountId,
|
|
195
426
|
log,
|
|
@@ -358,6 +589,8 @@ export async function startGateway(ctx) {
|
|
|
358
589
|
const ws = new WebSocket(gatewayUrl, { headers: { "User-Agent": PLUGIN_USER_AGENT } });
|
|
359
590
|
currentWs = ws;
|
|
360
591
|
const pluginRuntime = getQQBotRuntime();
|
|
592
|
+
// 群历史消息缓存:非@消息写入此 Map,被@时一次性注入上下文后清空
|
|
593
|
+
const groupHistories = new Map();
|
|
361
594
|
// 处理收到的消息
|
|
362
595
|
const handleMessage = async (event) => {
|
|
363
596
|
log?.debug?.(`[qqbot:${account.accountId}] Received message: ${JSON.stringify(event)}`);
|
|
@@ -443,7 +676,7 @@ export async function startGateway(ctx) {
|
|
|
443
676
|
const hasAsrReferFallback = voiceTranscriptSources.includes("asr");
|
|
444
677
|
// 解析 QQ 表情标签,将 <faceType=...,ext="base64"> 替换为 【表情: 中文名】
|
|
445
678
|
const parsedContent = parseFaceTags(event.content);
|
|
446
|
-
|
|
679
|
+
let userContent = voiceText
|
|
447
680
|
? (parsedContent.trim() ? `${parsedContent}\n${voiceText}` : voiceText) + attachmentInfo
|
|
448
681
|
: parsedContent + attachmentInfo;
|
|
449
682
|
// ============ 引用消息处理 ============
|
|
@@ -544,8 +777,8 @@ export async function startGateway(ctx) {
|
|
|
544
777
|
}
|
|
545
778
|
}
|
|
546
779
|
// ============ 构建 contextInfo(静态/动态分离) ============
|
|
547
|
-
//
|
|
548
|
-
// -
|
|
780
|
+
// 设计原则:
|
|
781
|
+
// - 静态指引:每条消息不变的内容(场景锚定、投递地址、能力说明),
|
|
549
782
|
// 注入 systemPrompts 前部,session 中虽重复出现但 AI 会自动降权,
|
|
550
783
|
// 且保证长 session 窗口截断后仍可见。
|
|
551
784
|
// - 动态标签:每条消息变化的数据(时间、附件、ASR),
|
|
@@ -563,7 +796,7 @@ export async function startGateway(ctx) {
|
|
|
563
796
|
const staticInstruction = staticParts.join(" | ");
|
|
564
797
|
systemPrompts.unshift(staticInstruction);
|
|
565
798
|
}
|
|
566
|
-
// ---
|
|
799
|
+
// --- 动态上下文 ---
|
|
567
800
|
const dynLines = [];
|
|
568
801
|
if (imageUrls.length > 0) {
|
|
569
802
|
dynLines.push(`- 图片: ${imageUrls.join(", ")}`);
|
|
@@ -574,22 +807,241 @@ export async function startGateway(ctx) {
|
|
|
574
807
|
if (uniqueVoiceAsrReferTexts.length > 0) {
|
|
575
808
|
dynLines.push(`- ASR: ${uniqueVoiceAsrReferTexts.join(" | ")}`);
|
|
576
809
|
}
|
|
577
|
-
const dynamicCtx = dynLines.length > 0 ? dynLines.join("\n") + "\n" : "";
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
const
|
|
810
|
+
const dynamicCtx = dynLines.length > 0 ? dynLines.join("\n") + "\n\n" : "";
|
|
811
|
+
// --- 命令授权(所有消息类型共用,群消息门控也需要) ---
|
|
812
|
+
// allowFrom: ["*"] 表示允许所有人,否则检查 senderId 是否在 allowFrom 列表中
|
|
813
|
+
const allowFromList = account.config?.allowFrom ?? [];
|
|
814
|
+
const allowAll = allowFromList.length === 0 || allowFromList.some((entry) => entry === "*");
|
|
815
|
+
const commandAuthorized = allowAll || allowFromList.some((entry) => entry.toUpperCase() === event.senderId.toUpperCase());
|
|
816
|
+
// --- 群消息上下文:插件只提供策略,框架自动组装 hint ---
|
|
817
|
+
let groupSystemPrompt = "";
|
|
818
|
+
let wasMentioned = false;
|
|
819
|
+
let groupSubject = "";
|
|
820
|
+
let senderLabel = "";
|
|
821
|
+
if (event.type === "group" && event.groupOpenid) {
|
|
822
|
+
// 1. 群策略检查(直接用 config 工具函数,与 Discord 的 allow-list.ts 同理)
|
|
823
|
+
if (!isGroupAllowed(cfg, event.groupOpenid, account.accountId)) {
|
|
824
|
+
log?.info(`[qqbot:${account.accountId}] Group ${event.groupOpenid} not allowed by groupPolicy, skipping`);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
// 2. @检测(委托 mentions 适配器)
|
|
828
|
+
const mentionPatternsForDetect = resolveMentionPatterns(cfg, route.agentId);
|
|
829
|
+
wasMentioned = detectWasMentioned({
|
|
830
|
+
eventType: event.eventType,
|
|
831
|
+
mentions: event.mentions,
|
|
832
|
+
content: event.content,
|
|
833
|
+
mentionPatterns: mentionPatternsForDetect,
|
|
834
|
+
});
|
|
835
|
+
// 3. requireMention 门控
|
|
836
|
+
// 优先级:session store 中的 /activation 命令 > 配置文件 requireMention > 默认值
|
|
837
|
+
// 未被 @ 时:消息仍写入上下文(让 bot 拥有完整对话记忆),但不触发 AI 回复
|
|
838
|
+
const configRequireMention = qqbotPlugin.groups?.resolveRequireMention?.({
|
|
839
|
+
cfg: cfg,
|
|
840
|
+
accountId: account.accountId,
|
|
841
|
+
groupId: event.groupOpenid,
|
|
842
|
+
}) ?? true;
|
|
843
|
+
const activation = resolveGroupActivation({
|
|
844
|
+
cfg: cfg,
|
|
845
|
+
agentId: route.agentId,
|
|
846
|
+
sessionKey: route.sessionKey,
|
|
847
|
+
configRequireMention,
|
|
848
|
+
});
|
|
849
|
+
const requireMention = activation === "mention";
|
|
850
|
+
// 4. 隐式 mention:引用回复 bot 的消息视为隐式 mention
|
|
851
|
+
const implicitMention = resolveImplicitMention({
|
|
852
|
+
refMsgIdx: event.refMsgIdx,
|
|
853
|
+
getRefEntry: getRefIndex,
|
|
854
|
+
});
|
|
855
|
+
// 4.5 统一门控:ignoreOtherMentions → shouldBlock → mention 门控
|
|
856
|
+
// 三层判断收敛到 resolveGroupMessageGate()
|
|
857
|
+
const contentForCommand = event.content?.trim() ?? "";
|
|
858
|
+
const allowTextCommands = shouldHandleTextCommands(cfg);
|
|
859
|
+
const gate = resolveGroupMessageGate({
|
|
860
|
+
ignoreOtherMentions: resolveIgnoreOtherMentions(cfg, event.groupOpenid, account.accountId),
|
|
861
|
+
hasAnyMention: hasAnyMention({ mentions: event.mentions, content: event.content }),
|
|
862
|
+
wasMentioned,
|
|
863
|
+
implicitMention,
|
|
864
|
+
allowTextCommands,
|
|
865
|
+
isControlCommand: hasControlCommand(contentForCommand),
|
|
866
|
+
commandAuthorized,
|
|
867
|
+
requireMention,
|
|
868
|
+
canDetectMention: true,
|
|
869
|
+
});
|
|
870
|
+
if (gate.action === "drop_other_mention") {
|
|
871
|
+
// @了其他人但未 @bot:记录历史后丢弃
|
|
872
|
+
const historyLimit = resolveHistoryLimit(cfg, event.groupOpenid, account.accountId);
|
|
873
|
+
const senderForHistory = event.senderName
|
|
874
|
+
? `${event.senderName} (${event.senderId})`
|
|
875
|
+
: event.senderId;
|
|
876
|
+
const historyAttachments = toAttachmentSummaries(event.attachments);
|
|
877
|
+
recordPendingHistoryEntry({
|
|
878
|
+
historyMap: groupHistories,
|
|
879
|
+
historyKey: event.groupOpenid,
|
|
880
|
+
limit: historyLimit,
|
|
881
|
+
entry: {
|
|
882
|
+
sender: senderForHistory,
|
|
883
|
+
body: parseFaceTags(event.content),
|
|
884
|
+
timestamp: new Date(event.timestamp).getTime(),
|
|
885
|
+
messageId: event.messageId,
|
|
886
|
+
attachments: historyAttachments,
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
log?.info(`[qqbot:${account.accountId}] Group ${event.groupOpenid}: drop message (ignoreOtherMentions=true, other user mentioned, bot not mentioned)`);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (gate.action === "block_unauthorized_command") {
|
|
893
|
+
// 未授权控制命令:静默拦截,不交给 AI
|
|
894
|
+
log?.info(`[qqbot:${account.accountId}] Group ${event.groupOpenid}: blocked unauthorized control command from ${event.senderId}: ${contentForCommand.slice(0, 50)}`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (gate.action === "skip_no_mention") {
|
|
898
|
+
// 非 @bot 消息:记录到群历史缓存后跳过 AI
|
|
899
|
+
const historyLimit = resolveHistoryLimit(cfg, event.groupOpenid, account.accountId);
|
|
900
|
+
const senderForHistory = event.senderName
|
|
901
|
+
? `${event.senderName} (${event.senderId})`
|
|
902
|
+
: event.senderId;
|
|
903
|
+
const historyAttachments = toAttachmentSummaries(event.attachments);
|
|
904
|
+
recordPendingHistoryEntry({
|
|
905
|
+
historyMap: groupHistories,
|
|
906
|
+
historyKey: event.groupOpenid,
|
|
907
|
+
limit: historyLimit,
|
|
908
|
+
entry: {
|
|
909
|
+
sender: senderForHistory,
|
|
910
|
+
body: parseFaceTags(event.content),
|
|
911
|
+
timestamp: new Date(event.timestamp).getTime(),
|
|
912
|
+
messageId: event.messageId,
|
|
913
|
+
attachments: historyAttachments,
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
log?.info(`[qqbot:${account.accountId}] Group ${event.groupOpenid}: activation=${activation} (configRequireMention=${configRequireMention}) not mentioned, recorded to history (limit=${historyLimit}, cached=${(groupHistories.get(event.groupOpenid) ?? []).length}${historyAttachments ? `, attachments=${historyAttachments.length}` : ""})`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
// gate.action === "pass" — 更新 wasMentioned 为 effectiveWasMentioned(含 implicit + bypass)
|
|
920
|
+
wasMentioned = gate.effectiveWasMentioned;
|
|
921
|
+
// 5. 发送者标签
|
|
922
|
+
senderLabel = event.senderName
|
|
923
|
+
? `${event.senderName} (${event.senderId})`
|
|
924
|
+
: event.senderId;
|
|
925
|
+
// 6. 群名称(从 config 中读取,fallback 为 openid 前 8 位)
|
|
926
|
+
groupSubject = resolveGroupName(cfg, event.groupOpenid, account.accountId);
|
|
927
|
+
// 7. GroupSystemPrompt — 根据消息来源(机器人/人类)和 @状态 注入差异化 PE
|
|
928
|
+
// 基础提示从 resolveGroupIntroHint 获取(群名称、平台限制等静态信息),
|
|
929
|
+
// 然后根据运行时状态追加针对性行为指引。
|
|
930
|
+
const baseHint = qqbotPlugin.groups?.resolveGroupIntroHint?.({
|
|
931
|
+
cfg: cfg,
|
|
932
|
+
accountId: account.accountId,
|
|
933
|
+
groupId: event.groupOpenid,
|
|
934
|
+
}) ?? "";
|
|
935
|
+
let behaviorPrompt = "";
|
|
936
|
+
// 从配置读取群行为 PE
|
|
937
|
+
behaviorPrompt = resolveGroupPrompt(cfg, event.groupOpenid, account.accountId);
|
|
938
|
+
groupSystemPrompt = [baseHint, behaviorPrompt].filter(Boolean).join("\n");
|
|
939
|
+
}
|
|
940
|
+
const mergedCount = event._mergedCount;
|
|
941
|
+
// 将 <@member_openid> 替换为 @username(使用 mentions 适配器)
|
|
942
|
+
if (event.type === "group" && event.mentions?.length) {
|
|
943
|
+
userContent = stripMentionText(userContent, event.mentions) ?? userContent;
|
|
944
|
+
}
|
|
945
|
+
else if (event.mentions?.length) {
|
|
946
|
+
for (const m of event.mentions) {
|
|
947
|
+
if (m.member_openid && m.username) {
|
|
948
|
+
userContent = userContent.replace(new RegExp(`<@${m.member_openid}>`, "g"), `@${m.username}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// 群消息 user prompt 带上发送者昵称(合并消息已内嵌发送者前缀,不再重复添加)
|
|
953
|
+
const isMergedMsg = mergedCount && mergedCount > 1;
|
|
954
|
+
const senderPrefix = (event.type === "group" && !isMergedMsg)
|
|
955
|
+
? `[${event.senderName ? `${event.senderName} (${event.senderId})` : event.senderId}] `
|
|
956
|
+
: "";
|
|
957
|
+
const isAtYouTag = event.type === "group"
|
|
958
|
+
? (wasMentioned ? " (@你)" : "")
|
|
959
|
+
: "";
|
|
960
|
+
// 合并消息:前面的消息用 envelope 历史格式,最后一条用当前消息格式(与 mention 单条回复对齐)
|
|
961
|
+
// BodyForAgent 只包含动态上下文 + 用户消息,不拼入 systemPrompts。
|
|
962
|
+
// systemPrompts([QQBot] to=...、TTS 能力声明等)通过 GroupSystemPrompt 注入到
|
|
963
|
+
// 框架的 extraSystemPrompt 中,不会存入 transcript 的 user turn content,
|
|
964
|
+
// 避免 Web UI 不显示用户 query 的问题。
|
|
965
|
+
let userMessage;
|
|
966
|
+
const mergedMessages = event._mergedMessages;
|
|
967
|
+
if (isMergedMsg && mergedMessages?.length) {
|
|
968
|
+
// --- 辅助:格式化单条子消息内容(表情解析 + mention 清理 + 附件标签) ---
|
|
969
|
+
const formatSubMsgContent = (m) => formatMessageContent({
|
|
970
|
+
content: m.content ?? "",
|
|
971
|
+
chatType: m.type,
|
|
972
|
+
mentions: m.mentions,
|
|
973
|
+
attachments: m.attachments,
|
|
974
|
+
parseFaceTags,
|
|
975
|
+
stripMentionText: (text, mentions) => stripMentionText(text, mentions) ?? text,
|
|
976
|
+
});
|
|
977
|
+
// 前面的消息使用 envelope 历史格式
|
|
978
|
+
const preceding = mergedMessages.slice(0, -1);
|
|
979
|
+
const lastMsg = mergedMessages[mergedMessages.length - 1];
|
|
980
|
+
const envelopeParts = preceding.map((m) => {
|
|
981
|
+
const msgContent = formatSubMsgContent(m);
|
|
982
|
+
const senderName = m.senderName
|
|
983
|
+
? (m.senderName.includes(m.senderId) ? m.senderName : `${m.senderName} (${m.senderId})`)
|
|
984
|
+
: m.senderId;
|
|
985
|
+
return pluginRuntime.channel.reply.formatInboundEnvelope({
|
|
986
|
+
channel: "qqbot",
|
|
987
|
+
from: senderName,
|
|
988
|
+
timestamp: new Date(m.timestamp).getTime(),
|
|
989
|
+
body: msgContent,
|
|
990
|
+
chatType: "group",
|
|
991
|
+
envelope: envelopeOptions,
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
// 最后一条消息使用简洁格式:[发送者]: 内容 (@你)
|
|
995
|
+
const lastContent = formatSubMsgContent(lastMsg);
|
|
996
|
+
const lastSenderName = lastMsg.senderName
|
|
997
|
+
? (lastMsg.senderName.includes(lastMsg.senderId) ? lastMsg.senderName : `${lastMsg.senderName} (${lastMsg.senderId})`)
|
|
998
|
+
: lastMsg.senderId;
|
|
999
|
+
const lastPart = `[${lastSenderName}] ${lastContent}${isAtYouTag}`;
|
|
1000
|
+
// 前置消息用段落标签包裹(类似引用消息的 [引用消息开始]...[引用消息结束])
|
|
1001
|
+
userMessage = buildMergedMessageContext({
|
|
1002
|
+
precedingParts: envelopeParts,
|
|
1003
|
+
currentMessage: lastPart,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
// 命令直接透传,不注入上下文
|
|
1008
|
+
userMessage = senderPrefix ? `${senderPrefix}${quotePart}${userContent}${isAtYouTag}` : `${quotePart}${userContent}`;
|
|
1009
|
+
}
|
|
1010
|
+
let agentBody = userContent.startsWith("/")
|
|
581
1011
|
? userContent
|
|
582
|
-
: `${
|
|
1012
|
+
: `${dynamicCtx}${userMessage}`;
|
|
1013
|
+
// 被@时:将累积的非@历史消息注入上下文
|
|
1014
|
+
// 消息格式使用 formatInboundEnvelope 与正常消息保持一致
|
|
1015
|
+
if (event.type === "group" && event.groupOpenid) {
|
|
1016
|
+
const historyLimit = resolveHistoryLimit(cfg, event.groupOpenid, account.accountId);
|
|
1017
|
+
const envelopeOpts = pluginRuntime.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
1018
|
+
agentBody = buildPendingHistoryContext({
|
|
1019
|
+
historyMap: groupHistories,
|
|
1020
|
+
historyKey: event.groupOpenid,
|
|
1021
|
+
limit: historyLimit,
|
|
1022
|
+
currentMessage: agentBody,
|
|
1023
|
+
formatEntry: (entry) => {
|
|
1024
|
+
// 将附件描述追加到消息 body 末尾,确保富媒体上下文不丢失
|
|
1025
|
+
const attachmentDesc = formatAttachmentTags(entry.attachments);
|
|
1026
|
+
const bodyWithAttachments = attachmentDesc
|
|
1027
|
+
? `${entry.body} ${attachmentDesc}`
|
|
1028
|
+
: entry.body;
|
|
1029
|
+
return pluginRuntime.channel.reply.formatInboundEnvelope({
|
|
1030
|
+
channel: "qqbot",
|
|
1031
|
+
from: entry.sender,
|
|
1032
|
+
timestamp: entry.timestamp,
|
|
1033
|
+
body: bodyWithAttachments,
|
|
1034
|
+
chatType: "group",
|
|
1035
|
+
envelope: envelopeOpts,
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
583
1040
|
log?.info(`[qqbot:${account.accountId}] agentBody length: ${agentBody.length}`);
|
|
584
1041
|
const fromAddress = event.type === "guild" ? `qqbot:channel:${event.channelId}`
|
|
585
1042
|
: event.type === "group" ? `qqbot:group:${event.groupOpenid}`
|
|
586
1043
|
: `qqbot:c2c:${event.senderId}`;
|
|
587
1044
|
const toAddress = fromAddress;
|
|
588
|
-
// 计算命令授权状态
|
|
589
|
-
// allowFrom: ["*"] 表示允许所有人,否则检查 senderId 是否在 allowFrom 列表中
|
|
590
|
-
const allowFromList = account.config?.allowFrom ?? [];
|
|
591
|
-
const allowAll = allowFromList.length === 0 || allowFromList.some((entry) => entry === "*");
|
|
592
|
-
const commandAuthorized = allowAll || allowFromList.some((entry) => entry.toUpperCase() === event.senderId.toUpperCase());
|
|
593
1045
|
// 分离 imageUrls 为本地路径和远程 URL,供 openclaw 原生媒体处理
|
|
594
1046
|
const localMediaPaths = [];
|
|
595
1047
|
const localMediaTypes = [];
|
|
@@ -607,6 +1059,11 @@ export async function startGateway(ctx) {
|
|
|
607
1059
|
localMediaTypes.push(t);
|
|
608
1060
|
}
|
|
609
1061
|
}
|
|
1062
|
+
// QQBot 静态系统提示(投递地址、TTS 能力等)合并到 GroupSystemPrompt,
|
|
1063
|
+
// 通过框架的 extraSystemPrompt 机制注入 AI system prompt,
|
|
1064
|
+
// 不会存入 transcript 的 user turn content。
|
|
1065
|
+
const qqbotSystemInstruction = systemPrompts.length > 0 ? systemPrompts.join("\n") : "";
|
|
1066
|
+
const mergedGroupSystemPrompt = [qqbotSystemInstruction, groupSystemPrompt].filter(Boolean).join("\n") || undefined;
|
|
610
1067
|
const ctxPayload = pluginRuntime.channel.reply.finalizeInboundContext({
|
|
611
1068
|
Body: body,
|
|
612
1069
|
BodyForAgent: agentBody,
|
|
@@ -617,6 +1074,11 @@ export async function startGateway(ctx) {
|
|
|
617
1074
|
SessionKey: route.sessionKey,
|
|
618
1075
|
AccountId: route.accountId,
|
|
619
1076
|
ChatType: isGroupChat ? "group" : "direct",
|
|
1077
|
+
GroupSystemPrompt: mergedGroupSystemPrompt,
|
|
1078
|
+
// 群消息元数据(框架级字段)
|
|
1079
|
+
WasMentioned: isGroupChat ? wasMentioned : undefined,
|
|
1080
|
+
SenderLabel: isGroupChat ? senderLabel : undefined,
|
|
1081
|
+
GroupSubject: isGroupChat ? groupSubject : undefined,
|
|
620
1082
|
SenderId: event.senderId,
|
|
621
1083
|
SenderName: event.senderName,
|
|
622
1084
|
Provider: "qqbot",
|
|
@@ -646,7 +1108,7 @@ export async function startGateway(ctx) {
|
|
|
646
1108
|
MediaUrls: remoteMediaUrls,
|
|
647
1109
|
MediaUrl: remoteMediaUrls[0],
|
|
648
1110
|
} : {}),
|
|
649
|
-
//
|
|
1111
|
+
// 引用消息上下文
|
|
650
1112
|
...(replyToId ? {
|
|
651
1113
|
ReplyToId: replyToId,
|
|
652
1114
|
ReplyToBody: replyToBody,
|
|
@@ -669,7 +1131,7 @@ export async function startGateway(ctx) {
|
|
|
669
1131
|
const sendErrorMessage = (errorText) => sendErrorToTarget(replyCtx, errorText);
|
|
670
1132
|
// 使用 AsyncLocalStorage 建立请求级上下文,作用域内所有异步代码
|
|
671
1133
|
// (包括 AI agent 调用、tool execute)都能安全获取当前会话信息,无并发竞态。
|
|
672
|
-
await runWithRequestContext({ target: qualifiedTarget }, async () => {
|
|
1134
|
+
await runWithRequestContext({ target: qualifiedTarget, accountId: account.accountId }, async () => {
|
|
673
1135
|
try {
|
|
674
1136
|
const messagesConfig = pluginRuntime.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId);
|
|
675
1137
|
// 追踪是否有响应
|
|
@@ -896,6 +1358,14 @@ export async function startGateway(ctx) {
|
|
|
896
1358
|
// StreamingController 内部已有重试,这里只打日志
|
|
897
1359
|
log?.error(`[qqbot:${account.accountId}] Streaming deliver error: ${err}`);
|
|
898
1360
|
}
|
|
1361
|
+
let replyText = payload.text ?? "";
|
|
1362
|
+
// 群消息:模型回复 NO_REPLY 表示无需回复,跳过发送
|
|
1363
|
+
// 注意:核心框架的 reply-delivery 已会拦截 NO_REPLY,此处为双重保险
|
|
1364
|
+
const trimmedReply = replyText.trim();
|
|
1365
|
+
if (event.type === "group" && (trimmedReply === "NO_REPLY" || trimmedReply === "[SKIP]")) {
|
|
1366
|
+
log?.info(`[qqbot:${account.accountId}] Model decided to skip group message (token=${trimmedReply}) from ${event.senderId}: ${event.content?.slice(0, 50)}`);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
899
1369
|
// 检查是否因流式 API 不可用而需要降级(ensureStreamingStarted 全部失败)
|
|
900
1370
|
// 如果需要降级,不 return,让本次 deliver 的 payload.text(全量文本)继续走普通发送逻辑
|
|
901
1371
|
if (streamingController.shouldFallbackToStatic) {
|
|
@@ -1092,6 +1562,15 @@ export async function startGateway(ctx) {
|
|
|
1092
1562
|
if (streamingController?.shouldFallbackToStatic) {
|
|
1093
1563
|
log?.debug?.(`[qqbot:${account.accountId}] Streaming was degraded to static mode (no chunk sent successfully)`);
|
|
1094
1564
|
}
|
|
1565
|
+
// 回复完成后清空群历史缓存(每次回复后重新累积)
|
|
1566
|
+
if (event.type === "group" && event.groupOpenid) {
|
|
1567
|
+
const historyLimit = resolveHistoryLimit(cfg, event.groupOpenid, account.accountId);
|
|
1568
|
+
clearPendingHistory({
|
|
1569
|
+
historyMap: groupHistories,
|
|
1570
|
+
historyKey: event.groupOpenid,
|
|
1571
|
+
limit: historyLimit,
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1095
1574
|
}
|
|
1096
1575
|
}
|
|
1097
1576
|
catch (err) {
|
|
@@ -1301,10 +1780,11 @@ export async function startGateway(ctx) {
|
|
|
1301
1780
|
}
|
|
1302
1781
|
else if (t === "GROUP_AT_MESSAGE_CREATE") {
|
|
1303
1782
|
const event = d;
|
|
1304
|
-
//
|
|
1783
|
+
// 被 @ 的消息,直接入队回复
|
|
1305
1784
|
recordKnownUser({
|
|
1306
1785
|
openid: event.author.member_openid,
|
|
1307
1786
|
type: "group",
|
|
1787
|
+
nickname: event.author.username,
|
|
1308
1788
|
groupOpenid: event.group_openid,
|
|
1309
1789
|
accountId: account.accountId,
|
|
1310
1790
|
});
|
|
@@ -1312,6 +1792,7 @@ export async function startGateway(ctx) {
|
|
|
1312
1792
|
trySlashCommandOrEnqueue({
|
|
1313
1793
|
type: "group",
|
|
1314
1794
|
senderId: event.author.member_openid,
|
|
1795
|
+
senderName: event.author.username,
|
|
1315
1796
|
content: event.content,
|
|
1316
1797
|
messageId: event.id,
|
|
1317
1798
|
timestamp: event.timestamp,
|
|
@@ -1319,6 +1800,67 @@ export async function startGateway(ctx) {
|
|
|
1319
1800
|
attachments: event.attachments,
|
|
1320
1801
|
refMsgIdx: groupRefs.refMsgIdx,
|
|
1321
1802
|
msgIdx: groupRefs.msgIdx,
|
|
1803
|
+
eventType: "GROUP_AT_MESSAGE_CREATE",
|
|
1804
|
+
mentions: event.mentions,
|
|
1805
|
+
messageScene: event.message_scene,
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
else if (t === "GROUP_MESSAGE_CREATE") {
|
|
1809
|
+
const event = d;
|
|
1810
|
+
recordKnownUser({
|
|
1811
|
+
openid: event.author.member_openid,
|
|
1812
|
+
type: "group",
|
|
1813
|
+
nickname: event.author.username,
|
|
1814
|
+
groupOpenid: event.group_openid,
|
|
1815
|
+
accountId: account.accountId,
|
|
1816
|
+
});
|
|
1817
|
+
const groupRefs = parseRefIndices(event.message_scene?.ext);
|
|
1818
|
+
trySlashCommandOrEnqueue({
|
|
1819
|
+
type: "group",
|
|
1820
|
+
senderId: event.author.member_openid,
|
|
1821
|
+
senderName: event.author.username,
|
|
1822
|
+
senderIsBot: event.author.bot,
|
|
1823
|
+
content: event.content,
|
|
1824
|
+
messageId: event.id,
|
|
1825
|
+
timestamp: event.timestamp,
|
|
1826
|
+
groupOpenid: event.group_openid,
|
|
1827
|
+
attachments: event.attachments,
|
|
1828
|
+
refMsgIdx: groupRefs.refMsgIdx,
|
|
1829
|
+
msgIdx: groupRefs.msgIdx,
|
|
1830
|
+
eventType: "GROUP_MESSAGE_CREATE",
|
|
1831
|
+
mentions: event.mentions,
|
|
1832
|
+
messageScene: event.message_scene,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
else if (t === "GROUP_ADD_ROBOT") {
|
|
1836
|
+
const event = d;
|
|
1837
|
+
log?.info(`[qqbot:${account.accountId}] Bot added to group: ${event.group_openid} by ${event.op_member_openid}`);
|
|
1838
|
+
recordKnownUser({
|
|
1839
|
+
openid: event.op_member_openid,
|
|
1840
|
+
type: "group",
|
|
1841
|
+
groupOpenid: event.group_openid,
|
|
1842
|
+
accountId: account.accountId,
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
else if (t === "GROUP_DEL_ROBOT") {
|
|
1846
|
+
const event = d;
|
|
1847
|
+
log?.info(`[qqbot:${account.accountId}] Bot removed from group: ${event.group_openid} by ${event.op_member_openid}`);
|
|
1848
|
+
}
|
|
1849
|
+
else if (t === "GROUP_MSG_REJECT") {
|
|
1850
|
+
const event = d;
|
|
1851
|
+
log?.info(`[qqbot:${account.accountId}] Group ${event.group_openid} rejected bot proactive messages (by ${event.op_member_openid})`);
|
|
1852
|
+
}
|
|
1853
|
+
else if (t === "GROUP_MSG_RECEIVE") {
|
|
1854
|
+
const event = d;
|
|
1855
|
+
log?.info(`[qqbot:${account.accountId}] Group ${event.group_openid} accepted bot proactive messages (by ${event.op_member_openid})`);
|
|
1856
|
+
}
|
|
1857
|
+
else if (t === "INTERACTION_CREATE") {
|
|
1858
|
+
const event = d;
|
|
1859
|
+
const resolved = event.data?.resolved;
|
|
1860
|
+
const sceneDesc = event.scene ?? (event.chat_type === 0 ? "guild" : event.chat_type === 1 ? "group" : "c2c");
|
|
1861
|
+
log?.info(`[qqbot:${account.accountId}] Interaction: scene=${sceneDesc}, type=${event.data?.type}, button_id=${resolved?.button_id}, button_data=${resolved?.button_data}, user=${event.group_member_openid || event.user_openid || resolved?.user_id || "unknown"}`);
|
|
1862
|
+
handleInteractionCreate({ event, account, cfg, log }).catch((err) => {
|
|
1863
|
+
log?.error(`[qqbot:${account.accountId}] Failed to handle interaction ${event.id}: ${err}`);
|
|
1322
1864
|
});
|
|
1323
1865
|
}
|
|
1324
1866
|
break;
|
|
@@ -1354,7 +1896,7 @@ export async function startGateway(ctx) {
|
|
|
1354
1896
|
ws.on("close", (code, reason) => {
|
|
1355
1897
|
log?.info(`[qqbot:${account.accountId}] WebSocket closed: ${code} ${reason.toString()}`);
|
|
1356
1898
|
isConnecting = false; // 释放锁
|
|
1357
|
-
//
|
|
1899
|
+
// 根据错误码处理(见 QQ 官方文档)
|
|
1358
1900
|
// 4004: CODE_INVALID_TOKEN - Token 无效,需刷新 token 重新连接
|
|
1359
1901
|
// 4006: CODE_SESSION_NO_LONGER_VALID - 会话失效,需重新 identify
|
|
1360
1902
|
// 4007: CODE_INVALID_SEQ - Resume 时 seq 无效,需重新 identify
|