@leoqlin/openclaw-qqbot 1.6.7-alpha1
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/LICENSE +22 -0
- package/README.md +484 -0
- package/README.zh.md +479 -0
- package/bin/qqbot-cli.js +243 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +26 -0
- package/dist/src/admin-resolver.d.ts +33 -0
- package/dist/src/admin-resolver.js +157 -0
- package/dist/src/api.d.ts +301 -0
- package/dist/src/api.js +890 -0
- package/dist/src/channel.d.ts +29 -0
- package/dist/src/channel.js +452 -0
- package/dist/src/config.d.ts +56 -0
- package/dist/src/config.js +278 -0
- package/dist/src/credential-backup.d.ts +31 -0
- package/dist/src/credential-backup.js +66 -0
- package/dist/src/deliver-debounce.d.ts +74 -0
- package/dist/src/deliver-debounce.js +174 -0
- package/dist/src/gateway.d.ts +18 -0
- package/dist/src/gateway.js +2005 -0
- package/dist/src/group-history.d.ts +136 -0
- package/dist/src/group-history.js +226 -0
- package/dist/src/image-server.d.ts +87 -0
- package/dist/src/image-server.js +570 -0
- package/dist/src/inbound-attachments.d.ts +60 -0
- package/dist/src/inbound-attachments.js +248 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +263 -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 +89 -0
- package/dist/src/message-queue.js +257 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +203 -0
- package/dist/src/outbound-deliver.d.ts +48 -0
- package/dist/src/outbound-deliver.js +392 -0
- package/dist/src/outbound.d.ts +205 -0
- package/dist/src/outbound.js +938 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +399 -0
- package/dist/src/ref-index-store.d.ts +101 -0
- package/dist/src/ref-index-store.js +298 -0
- package/dist/src/reply-dispatcher.d.ts +35 -0
- package/dist/src/reply-dispatcher.js +311 -0
- package/dist/src/request-context.d.ts +25 -0
- package/dist/src/request-context.js +37 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/session-store.d.ts +52 -0
- package/dist/src/session-store.js +254 -0
- package/dist/src/slash-commands.d.ts +77 -0
- package/dist/src/slash-commands.js +1866 -0
- package/dist/src/startup-greeting.d.ts +30 -0
- package/dist/src/startup-greeting.js +97 -0
- package/dist/src/streaming.d.ts +247 -0
- package/dist/src/streaming.js +899 -0
- package/dist/src/stt.d.ts +21 -0
- package/dist/src/stt.js +70 -0
- package/dist/src/tools/channel.d.ts +16 -0
- package/dist/src/tools/channel.js +234 -0
- package/dist/src/tools/remind.d.ts +2 -0
- package/dist/src/tools/remind.js +256 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.js +17 -0
- package/dist/src/typing-keepalive.d.ts +27 -0
- package/dist/src/typing-keepalive.js +64 -0
- package/dist/src/update-checker.d.ts +36 -0
- package/dist/src/update-checker.js +171 -0
- package/dist/src/utils/audio-convert.d.ts +98 -0
- package/dist/src/utils/audio-convert.js +755 -0
- package/dist/src/utils/chunked-upload.d.ts +68 -0
- package/dist/src/utils/chunked-upload.js +341 -0
- package/dist/src/utils/file-utils.d.ts +61 -0
- package/dist/src/utils/file-utils.js +172 -0
- package/dist/src/utils/image-size.d.ts +51 -0
- package/dist/src/utils/image-size.js +234 -0
- package/dist/src/utils/media-send.d.ts +158 -0
- package/dist/src/utils/media-send.js +499 -0
- package/dist/src/utils/media-tags.d.ts +14 -0
- package/dist/src/utils/media-tags.js +165 -0
- package/dist/src/utils/payload.d.ts +112 -0
- package/dist/src/utils/payload.js +186 -0
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +61 -0
- package/dist/src/utils/platform.d.ts +137 -0
- package/dist/src/utils/platform.js +390 -0
- package/dist/src/utils/ssrf-guard.d.ts +25 -0
- package/dist/src/utils/ssrf-guard.js +91 -0
- package/dist/src/utils/text-parsing.d.ts +36 -0
- package/dist/src/utils/text-parsing.js +75 -0
- package/dist/src/utils/upload-cache.d.ts +34 -0
- package/dist/src/utils/upload-cache.js +93 -0
- package/index.ts +31 -0
- package/node_modules/@eshaz/web-worker/LICENSE +201 -0
- package/node_modules/@eshaz/web-worker/README.md +134 -0
- package/node_modules/@eshaz/web-worker/browser.js +17 -0
- package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
- package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
- package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
- package/node_modules/@eshaz/web-worker/node.js +223 -0
- package/node_modules/@eshaz/web-worker/package.json +54 -0
- package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
- package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
- package/node_modules/mpg123-decoder/README.md +265 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
- package/node_modules/mpg123-decoder/index.js +8 -0
- package/node_modules/mpg123-decoder/package.json +58 -0
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
- package/node_modules/mpg123-decoder/types.d.ts +30 -0
- package/node_modules/silk-wasm/LICENSE +21 -0
- package/node_modules/silk-wasm/README.md +85 -0
- package/node_modules/silk-wasm/lib/index.cjs +16 -0
- package/node_modules/silk-wasm/lib/index.d.ts +70 -0
- package/node_modules/silk-wasm/lib/index.mjs +16 -0
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
- package/node_modules/silk-wasm/package.json +39 -0
- package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
- package/node_modules/simple-yenc/.prettierignore +1 -0
- package/node_modules/simple-yenc/LICENSE +7 -0
- package/node_modules/simple-yenc/README.md +163 -0
- package/node_modules/simple-yenc/dist/esm.js +1 -0
- package/node_modules/simple-yenc/dist/index.js +1 -0
- package/node_modules/simple-yenc/package.json +50 -0
- package/node_modules/simple-yenc/rollup.config.js +27 -0
- package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +22 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +706 -0
- package/node_modules/ws/lib/sender.js +602 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +554 -0
- package/node_modules/ws/lib/websocket.js +1393 -0
- package/node_modules/ws/package.json +70 -0
- package/node_modules/ws/wrapper.mjs +21 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +70 -0
- package/preload.cjs +33 -0
- package/scripts/cleanup-legacy-plugins.sh +124 -0
- package/scripts/link-sdk-core.cjs +185 -0
- package/scripts/postinstall-link-sdk.js +126 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/set-markdown.sh +156 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/scripts/upgrade-via-npm.ps1 +460 -0
- package/scripts/upgrade-via-npm.sh +652 -0
- package/scripts/upgrade-via-source.sh +1026 -0
- package/skills/qqbot-channel/SKILL.md +263 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +60 -0
- package/skills/qqbot-remind/SKILL.md +159 -0
- package/src/admin-resolver.ts +181 -0
- package/src/api.ts +1284 -0
- package/src/channel.ts +477 -0
- package/src/config.ts +347 -0
- package/src/credential-backup.ts +72 -0
- package/src/deliver-debounce.ts +229 -0
- package/src/gateway.ts +2245 -0
- package/src/group-history.ts +328 -0
- package/src/image-server.ts +675 -0
- package/src/inbound-attachments.ts +321 -0
- package/src/known-users.ts +353 -0
- package/src/message-gating.ts +190 -0
- package/src/message-queue.ts +352 -0
- package/src/onboarding.ts +274 -0
- package/src/openclaw-plugin-sdk.d.ts +587 -0
- package/src/outbound-deliver.ts +473 -0
- package/src/outbound.ts +1131 -0
- package/src/proactive.ts +530 -0
- package/src/ref-index-store.ts +412 -0
- package/src/reply-dispatcher.ts +334 -0
- package/src/request-context.ts +49 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +303 -0
- package/src/slash-commands.ts +2030 -0
- package/src/startup-greeting.ts +120 -0
- package/src/streaming.ts +1077 -0
- package/src/stt.ts +86 -0
- package/src/tools/channel.ts +281 -0
- package/src/tools/remind.ts +308 -0
- package/src/types.ts +391 -0
- package/src/typing-keepalive.ts +59 -0
- package/src/update-checker.ts +186 -0
- package/src/utils/audio-convert.ts +859 -0
- package/src/utils/chunked-upload.ts +483 -0
- package/src/utils/file-utils.ts +193 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/media-send.ts +631 -0
- package/src/utils/media-tags.ts +183 -0
- package/src/utils/payload.ts +265 -0
- package/src/utils/pkg-version.ts +64 -0
- package/src/utils/platform.ts +435 -0
- package/src/utils/ssrf-guard.ts +102 -0
- package/src/utils/text-parsing.ts +85 -0
- package/src/utils/upload-cache.ts +128 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 解析 mentionPatterns(agent → global → 空数组)
|
|
3
|
+
*
|
|
4
|
+
* 优先级:
|
|
5
|
+
* 1. agents.list[agentId].groupChat.mentionPatterns
|
|
6
|
+
* 2. messages.groupChat.mentionPatterns
|
|
7
|
+
* 3. []
|
|
8
|
+
*/
|
|
9
|
+
export function resolveMentionPatterns(cfg, agentId) {
|
|
10
|
+
// 1. agent 级别
|
|
11
|
+
if (agentId) {
|
|
12
|
+
const agents = cfg.agents;
|
|
13
|
+
const entry = agents?.list?.find((a) => a.id?.trim().toLowerCase() === agentId.trim().toLowerCase());
|
|
14
|
+
const agentGroupChat = entry?.groupChat;
|
|
15
|
+
if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
|
|
16
|
+
return agentGroupChat.mentionPatterns ?? [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// 2. 全局级别
|
|
20
|
+
const globalGroupChat = cfg?.messages?.groupChat;
|
|
21
|
+
if (globalGroupChat && typeof globalGroupChat === "object" && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
|
|
22
|
+
return globalGroupChat.mentionPatterns ?? [];
|
|
23
|
+
}
|
|
24
|
+
// 3. 空数组
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
export const DEFAULT_ACCOUNT_ID = "default";
|
|
28
|
+
function evaluateMatchedGroupAccessForPolicy(params) {
|
|
29
|
+
if (params.groupPolicy === "disabled") {
|
|
30
|
+
return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
|
|
31
|
+
}
|
|
32
|
+
if (params.groupPolicy === "allowlist") {
|
|
33
|
+
if (params.requireMatchInput && !params.hasMatchInput) {
|
|
34
|
+
return { allowed: false, groupPolicy: params.groupPolicy, reason: "missing_match_input" };
|
|
35
|
+
}
|
|
36
|
+
if (!params.allowlistConfigured) {
|
|
37
|
+
return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
|
|
38
|
+
}
|
|
39
|
+
if (!params.allowlistMatched) {
|
|
40
|
+
return { allowed: false, groupPolicy: params.groupPolicy, reason: "not_allowlisted" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
|
|
44
|
+
}
|
|
45
|
+
// ============ 群消息策略 ============
|
|
46
|
+
const DEFAULT_GROUP_POLICY = "open";
|
|
47
|
+
/** 群历史缓存条数默认值 */
|
|
48
|
+
const DEFAULT_GROUP_HISTORY_LIMIT = 50;
|
|
49
|
+
const DEFAULT_GROUP_CONFIG = {
|
|
50
|
+
requireMention: true,
|
|
51
|
+
ignoreOtherMentions: false,
|
|
52
|
+
toolPolicy: "restricted",
|
|
53
|
+
name: "",
|
|
54
|
+
historyLimit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
55
|
+
};
|
|
56
|
+
/** 默认群消息行为 PE(可通过配置覆盖) */
|
|
57
|
+
const DEFAULT_GROUP_PROMPT = [
|
|
58
|
+
"若发送者为机器人,仅在对方明确@你提问或请求协助具体任务时,以简洁明了的内容回复,",
|
|
59
|
+
"避免与其他机器人产生抢答或多轮无意义对话。",
|
|
60
|
+
"在群聊中优先让人类用户的消息得到响应,机器人之间保持协作而非竞争,确保对话有序不刷屏。",
|
|
61
|
+
].join("");
|
|
62
|
+
/** 解析群消息策略 */
|
|
63
|
+
export function resolveGroupPolicy(cfg, accountId) {
|
|
64
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
65
|
+
return account.config?.groupPolicy ?? DEFAULT_GROUP_POLICY;
|
|
66
|
+
}
|
|
67
|
+
/** 解析群白名单(统一转大写) */
|
|
68
|
+
export function resolveGroupAllowFrom(cfg, accountId) {
|
|
69
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
70
|
+
return (account.config?.groupAllowFrom ?? []).map((id) => String(id).trim().toUpperCase());
|
|
71
|
+
}
|
|
72
|
+
/** 检查指定群是否被允许(使用标准策略引擎) */
|
|
73
|
+
export function isGroupAllowed(cfg, groupOpenid, accountId) {
|
|
74
|
+
const policy = resolveGroupPolicy(cfg, accountId);
|
|
75
|
+
const allowList = resolveGroupAllowFrom(cfg, accountId);
|
|
76
|
+
const allowlistConfigured = allowList.length > 0;
|
|
77
|
+
const allowlistMatched = allowList.some((id) => id === "*" || id === groupOpenid.toUpperCase());
|
|
78
|
+
return evaluateMatchedGroupAccessForPolicy({
|
|
79
|
+
groupPolicy: policy,
|
|
80
|
+
allowlistConfigured,
|
|
81
|
+
allowlistMatched,
|
|
82
|
+
}).allowed;
|
|
83
|
+
}
|
|
84
|
+
/** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 默认值) */
|
|
85
|
+
export function resolveGroupConfig(cfg, groupOpenid, accountId) {
|
|
86
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
87
|
+
const groups = account.config?.groups ?? {};
|
|
88
|
+
const wildcardCfg = groups["*"] ?? {};
|
|
89
|
+
const specificCfg = groups[groupOpenid] ?? {};
|
|
90
|
+
return {
|
|
91
|
+
requireMention: specificCfg.requireMention ?? wildcardCfg.requireMention ?? DEFAULT_GROUP_CONFIG.requireMention,
|
|
92
|
+
ignoreOtherMentions: specificCfg.ignoreOtherMentions ?? wildcardCfg.ignoreOtherMentions ?? DEFAULT_GROUP_CONFIG.ignoreOtherMentions,
|
|
93
|
+
toolPolicy: specificCfg.toolPolicy ?? wildcardCfg.toolPolicy ?? DEFAULT_GROUP_CONFIG.toolPolicy,
|
|
94
|
+
name: specificCfg.name ?? wildcardCfg.name ?? DEFAULT_GROUP_CONFIG.name,
|
|
95
|
+
prompt: specificCfg.prompt ?? wildcardCfg.prompt,
|
|
96
|
+
historyLimit: specificCfg.historyLimit ?? wildcardCfg.historyLimit ?? DEFAULT_GROUP_CONFIG.historyLimit,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** 解析群历史消息缓存条数 */
|
|
100
|
+
export function resolveHistoryLimit(cfg, groupOpenid, accountId) {
|
|
101
|
+
return Math.max(0, resolveGroupConfig(cfg, groupOpenid, accountId).historyLimit);
|
|
102
|
+
}
|
|
103
|
+
/** 解析群行为 PE(具体群 > "*" > 默认值) */
|
|
104
|
+
export function resolveGroupPrompt(cfg, groupOpenid, accountId) {
|
|
105
|
+
const account = resolveQQBotAccount(cfg, accountId);
|
|
106
|
+
const groups = account.config?.groups ?? {};
|
|
107
|
+
return groups[groupOpenid]?.prompt ?? groups["*"]?.prompt ?? DEFAULT_GROUP_PROMPT;
|
|
108
|
+
}
|
|
109
|
+
/** 解析群是否需要 @机器人才响应 */
|
|
110
|
+
export function resolveRequireMention(cfg, groupOpenid, accountId) {
|
|
111
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).requireMention;
|
|
112
|
+
}
|
|
113
|
+
/** 解析群是否忽略 @了其他人(非 bot)的消息 */
|
|
114
|
+
export function resolveIgnoreOtherMentions(cfg, groupOpenid, accountId) {
|
|
115
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).ignoreOtherMentions;
|
|
116
|
+
}
|
|
117
|
+
/** 解析群工具策略 */
|
|
118
|
+
export function resolveToolPolicy(cfg, groupOpenid, accountId) {
|
|
119
|
+
return resolveGroupConfig(cfg, groupOpenid, accountId).toolPolicy;
|
|
120
|
+
}
|
|
121
|
+
/** 解析群名称(优先配置,fallback 为 openid 前 8 位) */
|
|
122
|
+
export function resolveGroupName(cfg, groupOpenid, accountId) {
|
|
123
|
+
const name = resolveGroupConfig(cfg, groupOpenid, accountId).name;
|
|
124
|
+
return name || groupOpenid.slice(0, 8);
|
|
125
|
+
}
|
|
126
|
+
function normalizeAppId(raw) {
|
|
127
|
+
if (raw === null || raw === undefined)
|
|
128
|
+
return "";
|
|
129
|
+
return String(raw).trim();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 列出所有 QQBot 账户 ID
|
|
133
|
+
*/
|
|
134
|
+
export function listQQBotAccountIds(cfg) {
|
|
135
|
+
const ids = new Set();
|
|
136
|
+
const qqbot = cfg.channels?.qqbot;
|
|
137
|
+
if (qqbot?.appId) {
|
|
138
|
+
ids.add(DEFAULT_ACCOUNT_ID);
|
|
139
|
+
}
|
|
140
|
+
if (qqbot?.accounts) {
|
|
141
|
+
for (const accountId of Object.keys(qqbot.accounts)) {
|
|
142
|
+
if (qqbot.accounts[accountId]?.appId) {
|
|
143
|
+
ids.add(accountId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return Array.from(ids);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 获取默认账户 ID
|
|
151
|
+
*/
|
|
152
|
+
export function resolveDefaultQQBotAccountId(cfg) {
|
|
153
|
+
const qqbot = cfg.channels?.qqbot;
|
|
154
|
+
// 如果有默认账户配置,返回 default
|
|
155
|
+
if (qqbot?.appId) {
|
|
156
|
+
return DEFAULT_ACCOUNT_ID;
|
|
157
|
+
}
|
|
158
|
+
// 否则返回第一个配置的账户
|
|
159
|
+
if (qqbot?.accounts) {
|
|
160
|
+
const ids = Object.keys(qqbot.accounts);
|
|
161
|
+
if (ids.length > 0) {
|
|
162
|
+
return ids[0];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return DEFAULT_ACCOUNT_ID;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 解析 QQBot 账户配置
|
|
169
|
+
*/
|
|
170
|
+
export function resolveQQBotAccount(cfg, accountId) {
|
|
171
|
+
const resolvedAccountId = accountId ?? resolveDefaultQQBotAccountId(cfg);
|
|
172
|
+
const qqbot = cfg.channels?.qqbot;
|
|
173
|
+
// 基础配置
|
|
174
|
+
let accountConfig = {};
|
|
175
|
+
let appId = "";
|
|
176
|
+
let clientSecret = "";
|
|
177
|
+
let secretSource = "none";
|
|
178
|
+
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
179
|
+
// 默认账户从顶层读取(展开所有字段,避免遗漏新增配置项)
|
|
180
|
+
const { accounts: _accounts, ...topLevelConfig } = qqbot ?? {};
|
|
181
|
+
accountConfig = {
|
|
182
|
+
...topLevelConfig,
|
|
183
|
+
markdownSupport: qqbot?.markdownSupport ?? true,
|
|
184
|
+
};
|
|
185
|
+
appId = normalizeAppId(qqbot?.appId);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// 命名账户从 accounts 读取
|
|
189
|
+
const account = qqbot?.accounts?.[resolvedAccountId];
|
|
190
|
+
accountConfig = account ?? {};
|
|
191
|
+
appId = normalizeAppId(account?.appId);
|
|
192
|
+
}
|
|
193
|
+
// 解析 clientSecret
|
|
194
|
+
if (accountConfig.clientSecret) {
|
|
195
|
+
clientSecret = accountConfig.clientSecret;
|
|
196
|
+
secretSource = "config";
|
|
197
|
+
}
|
|
198
|
+
else if (accountConfig.clientSecretFile) {
|
|
199
|
+
// 从文件读取(运行时处理)
|
|
200
|
+
secretSource = "file";
|
|
201
|
+
}
|
|
202
|
+
else if (process.env.QQBOT_CLIENT_SECRET && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
203
|
+
clientSecret = process.env.QQBOT_CLIENT_SECRET;
|
|
204
|
+
secretSource = "env";
|
|
205
|
+
}
|
|
206
|
+
// AppId 也可以从环境变量读取
|
|
207
|
+
if (!appId && process.env.QQBOT_APP_ID && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
208
|
+
appId = normalizeAppId(process.env.QQBOT_APP_ID);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
accountId: resolvedAccountId,
|
|
212
|
+
name: accountConfig.name,
|
|
213
|
+
enabled: accountConfig.enabled !== false,
|
|
214
|
+
appId,
|
|
215
|
+
clientSecret,
|
|
216
|
+
secretSource,
|
|
217
|
+
systemPrompt: accountConfig.systemPrompt,
|
|
218
|
+
imageServerBaseUrl: accountConfig.imageServerBaseUrl || process.env.QQBOT_IMAGE_SERVER_BASE_URL,
|
|
219
|
+
markdownSupport: accountConfig.markdownSupport !== false,
|
|
220
|
+
config: accountConfig,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 应用账户配置
|
|
225
|
+
*/
|
|
226
|
+
export function applyQQBotAccountConfig(cfg, accountId, input) {
|
|
227
|
+
const next = { ...cfg };
|
|
228
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
229
|
+
// 如果没有设置过 allowFrom,默认设置为 ["*"]
|
|
230
|
+
const existingConfig = next.channels?.qqbot || {};
|
|
231
|
+
const allowFrom = existingConfig.allowFrom ?? ["*"];
|
|
232
|
+
next.channels = {
|
|
233
|
+
...next.channels,
|
|
234
|
+
qqbot: {
|
|
235
|
+
...(next.channels?.qqbot || {}),
|
|
236
|
+
enabled: true,
|
|
237
|
+
allowFrom,
|
|
238
|
+
...(input.appId ? { appId: input.appId } : {}),
|
|
239
|
+
...(input.clientSecret
|
|
240
|
+
? { clientSecret: input.clientSecret }
|
|
241
|
+
: input.clientSecretFile
|
|
242
|
+
? { clientSecretFile: input.clientSecretFile }
|
|
243
|
+
: {}),
|
|
244
|
+
...(input.name ? { name: input.name } : {}),
|
|
245
|
+
...(input.imageServerBaseUrl ? { imageServerBaseUrl: input.imageServerBaseUrl } : {}),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// 如果没有设置过 allowFrom,默认设置为 ["*"]
|
|
251
|
+
const existingAccountConfig = next.channels?.qqbot?.accounts?.[accountId] || {};
|
|
252
|
+
const allowFrom = existingAccountConfig.allowFrom ?? ["*"];
|
|
253
|
+
next.channels = {
|
|
254
|
+
...next.channels,
|
|
255
|
+
qqbot: {
|
|
256
|
+
...(next.channels?.qqbot || {}),
|
|
257
|
+
enabled: true,
|
|
258
|
+
accounts: {
|
|
259
|
+
...(next.channels?.qqbot?.accounts || {}),
|
|
260
|
+
[accountId]: {
|
|
261
|
+
...(next.channels?.qqbot?.accounts?.[accountId] || {}),
|
|
262
|
+
enabled: true,
|
|
263
|
+
allowFrom,
|
|
264
|
+
...(input.appId ? { appId: input.appId } : {}),
|
|
265
|
+
...(input.clientSecret
|
|
266
|
+
? { clientSecret: input.clientSecret }
|
|
267
|
+
: input.clientSecretFile
|
|
268
|
+
? { clientSecretFile: input.clientSecretFile }
|
|
269
|
+
: {}),
|
|
270
|
+
...(input.name ? { name: input.name } : {}),
|
|
271
|
+
...(input.imageServerBaseUrl ? { imageServerBaseUrl: input.imageServerBaseUrl } : {}),
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return next;
|
|
278
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 凭证暂存与恢复
|
|
3
|
+
*
|
|
4
|
+
* 解决热更新被打断时 openclaw.json 中 appId/secret 丢失的问题。
|
|
5
|
+
*
|
|
6
|
+
* 原理:
|
|
7
|
+
* - 每次 gateway 成功启动后,把当前账户的 appId/secret 写入暂存文件
|
|
8
|
+
* - 插件启动时如果检测到配置中 appId/secret 为空,尝试从暂存文件恢复
|
|
9
|
+
* - 暂存文件存储在 ~/.openclaw/qqbot/data/ 下,不受插件目录替换影响
|
|
10
|
+
*
|
|
11
|
+
* 安全保障:
|
|
12
|
+
* - 只在 appId/secret **确实为空** 时才尝试恢复(不干扰正常配置变更)
|
|
13
|
+
* - 恢复后通过 openclaw 的 config API 写回配置文件,确保框架感知到变更
|
|
14
|
+
* - 暂存文件使用原子写入(先写 .tmp 再 rename)防止损坏
|
|
15
|
+
*/
|
|
16
|
+
interface CredentialBackup {
|
|
17
|
+
accountId: string;
|
|
18
|
+
appId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
savedAt: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 保存凭证快照到暂存文件(gateway 成功启动后调用)
|
|
24
|
+
*/
|
|
25
|
+
export declare function saveCredentialBackup(accountId: string, appId: string, clientSecret: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* 从暂存文件读取凭证(仅在配置为空时调用)
|
|
28
|
+
* 返回 null 表示无可用备份
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadCredentialBackup(accountId?: string): CredentialBackup | null;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 凭证暂存与恢复
|
|
3
|
+
*
|
|
4
|
+
* 解决热更新被打断时 openclaw.json 中 appId/secret 丢失的问题。
|
|
5
|
+
*
|
|
6
|
+
* 原理:
|
|
7
|
+
* - 每次 gateway 成功启动后,把当前账户的 appId/secret 写入暂存文件
|
|
8
|
+
* - 插件启动时如果检测到配置中 appId/secret 为空,尝试从暂存文件恢复
|
|
9
|
+
* - 暂存文件存储在 ~/.openclaw/qqbot/data/ 下,不受插件目录替换影响
|
|
10
|
+
*
|
|
11
|
+
* 安全保障:
|
|
12
|
+
* - 只在 appId/secret **确实为空** 时才尝试恢复(不干扰正常配置变更)
|
|
13
|
+
* - 恢复后通过 openclaw 的 config API 写回配置文件,确保框架感知到变更
|
|
14
|
+
* - 暂存文件使用原子写入(先写 .tmp 再 rename)防止损坏
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { getQQBotDataDir } from "./utils/platform.js";
|
|
19
|
+
const BACKUP_FILENAME = "credential-backup.json";
|
|
20
|
+
function getBackupPath() {
|
|
21
|
+
return path.join(getQQBotDataDir("data"), BACKUP_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 保存凭证快照到暂存文件(gateway 成功启动后调用)
|
|
25
|
+
*/
|
|
26
|
+
export function saveCredentialBackup(accountId, appId, clientSecret) {
|
|
27
|
+
if (!appId || !clientSecret)
|
|
28
|
+
return; // 不保存空凭证
|
|
29
|
+
try {
|
|
30
|
+
const backupPath = getBackupPath();
|
|
31
|
+
const data = {
|
|
32
|
+
accountId,
|
|
33
|
+
appId,
|
|
34
|
+
clientSecret,
|
|
35
|
+
savedAt: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
const tmpPath = backupPath + ".tmp";
|
|
38
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
39
|
+
fs.renameSync(tmpPath, backupPath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// 非关键操作,静默忽略
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 从暂存文件读取凭证(仅在配置为空时调用)
|
|
47
|
+
* 返回 null 表示无可用备份
|
|
48
|
+
*/
|
|
49
|
+
export function loadCredentialBackup(accountId) {
|
|
50
|
+
try {
|
|
51
|
+
const backupPath = getBackupPath();
|
|
52
|
+
if (!fs.existsSync(backupPath))
|
|
53
|
+
return null;
|
|
54
|
+
const raw = fs.readFileSync(backupPath, "utf8");
|
|
55
|
+
const data = JSON.parse(raw);
|
|
56
|
+
if (!data.appId || !data.clientSecret)
|
|
57
|
+
return null;
|
|
58
|
+
// 如果指定了 accountId,校验是否匹配
|
|
59
|
+
if (accountId && data.accountId !== accountId)
|
|
60
|
+
return null;
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出站消息合并回复(Deliver Debounce)模块
|
|
3
|
+
*
|
|
4
|
+
* 解决的问题:
|
|
5
|
+
* 当 openclaw 框架层的 embedded agent 超时或快速连续产生多次 deliver 时,
|
|
6
|
+
* 用户会在短时间内收到大量碎片消息(消息轰炸)。
|
|
7
|
+
*
|
|
8
|
+
* 解决方案:
|
|
9
|
+
* 在 deliver 回调和实际发送之间加入 debounce 层。
|
|
10
|
+
* 短时间内(windowMs)连续到达的多条纯文本 deliver 会被合并为一条消息发送。
|
|
11
|
+
* 含媒体的 deliver 会立即 flush 已缓冲的文本并正常处理媒体。
|
|
12
|
+
*/
|
|
13
|
+
import type { DeliverDebounceConfig } from "./types.js";
|
|
14
|
+
export interface DeliverPayload {
|
|
15
|
+
text?: string;
|
|
16
|
+
mediaUrls?: string[];
|
|
17
|
+
mediaUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DeliverInfo {
|
|
20
|
+
kind: string;
|
|
21
|
+
}
|
|
22
|
+
/** 实际执行发送的回调 */
|
|
23
|
+
export type DeliverExecutor = (payload: DeliverPayload, info: DeliverInfo) => Promise<void>;
|
|
24
|
+
export declare class DeliverDebouncer {
|
|
25
|
+
private readonly windowMs;
|
|
26
|
+
private readonly maxWaitMs;
|
|
27
|
+
private readonly separator;
|
|
28
|
+
private readonly executor;
|
|
29
|
+
private readonly log?;
|
|
30
|
+
private readonly prefix;
|
|
31
|
+
/** 缓冲中的文本片段 */
|
|
32
|
+
private bufferedTexts;
|
|
33
|
+
/** 缓冲中最后一次 deliver 的 info(用于 flush 时传递 kind) */
|
|
34
|
+
private lastInfo;
|
|
35
|
+
/** 缓冲中最后一次 deliver 的 payload(非文本字段,如 mediaUrls) */
|
|
36
|
+
private lastPayload;
|
|
37
|
+
/** debounce 定时器 */
|
|
38
|
+
private debounceTimer;
|
|
39
|
+
/** 最大等待定时器(从第一条 deliver 开始计算) */
|
|
40
|
+
private maxWaitTimer;
|
|
41
|
+
/** 是否正在 flush */
|
|
42
|
+
private flushing;
|
|
43
|
+
/** 已销毁标记 */
|
|
44
|
+
private disposed;
|
|
45
|
+
constructor(config: DeliverDebounceConfig | undefined, executor: DeliverExecutor, log?: {
|
|
46
|
+
info: (msg: string) => void;
|
|
47
|
+
error: (msg: string) => void;
|
|
48
|
+
}, prefix?: string);
|
|
49
|
+
/**
|
|
50
|
+
* 接收一次 deliver 调用。
|
|
51
|
+
* - 纯文本 deliver → 缓冲并设置 debounce 定时器
|
|
52
|
+
* - 含媒体 deliver → 先 flush 已缓冲文本,再直接执行当前 deliver
|
|
53
|
+
*/
|
|
54
|
+
deliver(payload: DeliverPayload, info: DeliverInfo): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* 将缓冲中的文本合并为一条消息发送
|
|
57
|
+
*/
|
|
58
|
+
flush(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* 销毁:flush 剩余缓冲并清除定时器
|
|
61
|
+
*/
|
|
62
|
+
dispose(): Promise<void>;
|
|
63
|
+
/** 当前是否有缓冲中的文本 */
|
|
64
|
+
get hasPending(): boolean;
|
|
65
|
+
/** 缓冲中的文本数量 */
|
|
66
|
+
get pendingCount(): number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 根据配置创建 debouncer 或返回 null(禁用时)
|
|
70
|
+
*/
|
|
71
|
+
export declare function createDeliverDebouncer(config: DeliverDebounceConfig | undefined, executor: DeliverExecutor, log?: {
|
|
72
|
+
info: (msg: string) => void;
|
|
73
|
+
error: (msg: string) => void;
|
|
74
|
+
}, prefix?: string): DeliverDebouncer | null;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出站消息合并回复(Deliver Debounce)模块
|
|
3
|
+
*
|
|
4
|
+
* 解决的问题:
|
|
5
|
+
* 当 openclaw 框架层的 embedded agent 超时或快速连续产生多次 deliver 时,
|
|
6
|
+
* 用户会在短时间内收到大量碎片消息(消息轰炸)。
|
|
7
|
+
*
|
|
8
|
+
* 解决方案:
|
|
9
|
+
* 在 deliver 回调和实际发送之间加入 debounce 层。
|
|
10
|
+
* 短时间内(windowMs)连续到达的多条纯文本 deliver 会被合并为一条消息发送。
|
|
11
|
+
* 含媒体的 deliver 会立即 flush 已缓冲的文本并正常处理媒体。
|
|
12
|
+
*/
|
|
13
|
+
// ============ 默认值 ============
|
|
14
|
+
const DEFAULT_WINDOW_MS = 1500;
|
|
15
|
+
const DEFAULT_MAX_WAIT_MS = 8000;
|
|
16
|
+
const DEFAULT_SEPARATOR = "\n\n---\n\n";
|
|
17
|
+
// ============ DeliverDebouncer 类 ============
|
|
18
|
+
export class DeliverDebouncer {
|
|
19
|
+
windowMs;
|
|
20
|
+
maxWaitMs;
|
|
21
|
+
separator;
|
|
22
|
+
executor;
|
|
23
|
+
log;
|
|
24
|
+
prefix;
|
|
25
|
+
/** 缓冲中的文本片段 */
|
|
26
|
+
bufferedTexts = [];
|
|
27
|
+
/** 缓冲中最后一次 deliver 的 info(用于 flush 时传递 kind) */
|
|
28
|
+
lastInfo = null;
|
|
29
|
+
/** 缓冲中最后一次 deliver 的 payload(非文本字段,如 mediaUrls) */
|
|
30
|
+
lastPayload = null;
|
|
31
|
+
/** debounce 定时器 */
|
|
32
|
+
debounceTimer = null;
|
|
33
|
+
/** 最大等待定时器(从第一条 deliver 开始计算) */
|
|
34
|
+
maxWaitTimer = null;
|
|
35
|
+
/** 是否正在 flush */
|
|
36
|
+
flushing = false;
|
|
37
|
+
/** 已销毁标记 */
|
|
38
|
+
disposed = false;
|
|
39
|
+
constructor(config, executor, log, prefix = "[debounce]") {
|
|
40
|
+
this.windowMs = config?.windowMs ?? DEFAULT_WINDOW_MS;
|
|
41
|
+
this.maxWaitMs = config?.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
42
|
+
this.separator = config?.separator ?? DEFAULT_SEPARATOR;
|
|
43
|
+
this.executor = executor;
|
|
44
|
+
this.log = log;
|
|
45
|
+
this.prefix = prefix;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 接收一次 deliver 调用。
|
|
49
|
+
* - 纯文本 deliver → 缓冲并设置 debounce 定时器
|
|
50
|
+
* - 含媒体 deliver → 先 flush 已缓冲文本,再直接执行当前 deliver
|
|
51
|
+
*/
|
|
52
|
+
async deliver(payload, info) {
|
|
53
|
+
if (this.disposed)
|
|
54
|
+
return;
|
|
55
|
+
const hasMedia = Boolean((payload.mediaUrls && payload.mediaUrls.length > 0) || payload.mediaUrl);
|
|
56
|
+
const text = (payload.text ?? "").trim();
|
|
57
|
+
// 含媒体的 deliver:立即 flush 缓冲 + 直接执行
|
|
58
|
+
if (hasMedia) {
|
|
59
|
+
this.log?.info(`${this.prefix} Media deliver detected, flushing ${this.bufferedTexts.length} buffered text(s) first`);
|
|
60
|
+
await this.flush();
|
|
61
|
+
await this.executor(payload, info);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// 空文本 deliver:直接透传(不缓冲)
|
|
65
|
+
if (!text) {
|
|
66
|
+
await this.executor(payload, info);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// 纯文本 deliver:缓冲
|
|
70
|
+
this.bufferedTexts.push(text);
|
|
71
|
+
this.lastInfo = info;
|
|
72
|
+
this.lastPayload = payload;
|
|
73
|
+
this.log?.info(`${this.prefix} Buffered text #${this.bufferedTexts.length} (${text.length} chars), window=${this.windowMs}ms`);
|
|
74
|
+
// 重置 debounce 定时器
|
|
75
|
+
if (this.debounceTimer) {
|
|
76
|
+
clearTimeout(this.debounceTimer);
|
|
77
|
+
}
|
|
78
|
+
this.debounceTimer = setTimeout(() => {
|
|
79
|
+
this.flush().catch((err) => {
|
|
80
|
+
this.log?.error(`${this.prefix} Flush error (debounce timer): ${err}`);
|
|
81
|
+
});
|
|
82
|
+
}, this.windowMs);
|
|
83
|
+
// 首次缓冲时启动最大等待定时器
|
|
84
|
+
if (this.bufferedTexts.length === 1) {
|
|
85
|
+
if (this.maxWaitTimer) {
|
|
86
|
+
clearTimeout(this.maxWaitTimer);
|
|
87
|
+
}
|
|
88
|
+
this.maxWaitTimer = setTimeout(() => {
|
|
89
|
+
this.log?.info(`${this.prefix} Max wait (${this.maxWaitMs}ms) reached, force flushing`);
|
|
90
|
+
this.flush().catch((err) => {
|
|
91
|
+
this.log?.error(`${this.prefix} Flush error (max wait timer): ${err}`);
|
|
92
|
+
});
|
|
93
|
+
}, this.maxWaitMs);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 将缓冲中的文本合并为一条消息发送
|
|
98
|
+
*/
|
|
99
|
+
async flush() {
|
|
100
|
+
if (this.flushing || this.bufferedTexts.length === 0)
|
|
101
|
+
return;
|
|
102
|
+
this.flushing = true;
|
|
103
|
+
// 清除定时器
|
|
104
|
+
if (this.debounceTimer) {
|
|
105
|
+
clearTimeout(this.debounceTimer);
|
|
106
|
+
this.debounceTimer = null;
|
|
107
|
+
}
|
|
108
|
+
if (this.maxWaitTimer) {
|
|
109
|
+
clearTimeout(this.maxWaitTimer);
|
|
110
|
+
this.maxWaitTimer = null;
|
|
111
|
+
}
|
|
112
|
+
// 取出缓冲
|
|
113
|
+
const texts = this.bufferedTexts;
|
|
114
|
+
const info = this.lastInfo;
|
|
115
|
+
const lastPayload = this.lastPayload;
|
|
116
|
+
this.bufferedTexts = [];
|
|
117
|
+
this.lastInfo = null;
|
|
118
|
+
this.lastPayload = null;
|
|
119
|
+
try {
|
|
120
|
+
if (texts.length === 1) {
|
|
121
|
+
// 只有一条,直接透传原始 payload
|
|
122
|
+
this.log?.info(`${this.prefix} Flushing single buffered text (${texts[0].length} chars)`);
|
|
123
|
+
await this.executor({ ...lastPayload, text: texts[0] }, info);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// 多条合并
|
|
127
|
+
const merged = texts.join(this.separator);
|
|
128
|
+
this.log?.info(`${this.prefix} Merged ${texts.length} buffered texts into one (${merged.length} chars)`);
|
|
129
|
+
await this.executor({ ...lastPayload, text: merged }, info);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this.flushing = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 销毁:flush 剩余缓冲并清除定时器
|
|
138
|
+
*/
|
|
139
|
+
async dispose() {
|
|
140
|
+
this.disposed = true;
|
|
141
|
+
if (this.debounceTimer) {
|
|
142
|
+
clearTimeout(this.debounceTimer);
|
|
143
|
+
this.debounceTimer = null;
|
|
144
|
+
}
|
|
145
|
+
if (this.maxWaitTimer) {
|
|
146
|
+
clearTimeout(this.maxWaitTimer);
|
|
147
|
+
this.maxWaitTimer = null;
|
|
148
|
+
}
|
|
149
|
+
// flush 剩余
|
|
150
|
+
if (this.bufferedTexts.length > 0) {
|
|
151
|
+
this.flushing = false; // 确保 flush 能执行
|
|
152
|
+
await this.flush();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** 当前是否有缓冲中的文本 */
|
|
156
|
+
get hasPending() {
|
|
157
|
+
return this.bufferedTexts.length > 0;
|
|
158
|
+
}
|
|
159
|
+
/** 缓冲中的文本数量 */
|
|
160
|
+
get pendingCount() {
|
|
161
|
+
return this.bufferedTexts.length;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ============ 工厂函数 ============
|
|
165
|
+
/**
|
|
166
|
+
* 根据配置创建 debouncer 或返回 null(禁用时)
|
|
167
|
+
*/
|
|
168
|
+
export function createDeliverDebouncer(config, executor, log, prefix) {
|
|
169
|
+
// 未配置时默认启用
|
|
170
|
+
if (config?.enabled === false) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return new DeliverDebouncer(config, executor, log, prefix);
|
|
174
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResolvedQQBotAccount } from "./types.js";
|
|
2
|
+
export interface GatewayContext {
|
|
3
|
+
account: ResolvedQQBotAccount;
|
|
4
|
+
abortSignal: AbortSignal;
|
|
5
|
+
cfg: unknown;
|
|
6
|
+
onReady?: (data: unknown) => void;
|
|
7
|
+
onError?: (error: Error) => void;
|
|
8
|
+
log?: {
|
|
9
|
+
info: (msg: string) => void;
|
|
10
|
+
error: (msg: string) => void;
|
|
11
|
+
debug?: (msg: string) => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 启动 Gateway WebSocket 连接(带自动重连)
|
|
16
|
+
* 支持流式消息发送
|
|
17
|
+
*/
|
|
18
|
+
export declare function startGateway(ctx: GatewayContext): Promise<void>;
|