@tencent-connect/openclaw-qqbot 1.6.2-alpha.2 → 1.6.3-alpha.channel
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 +12 -12
- package/README.zh.md +12 -12
- package/dist/src/channel.d.ts +8 -0
- package/dist/src/channel.js +11 -29
- package/dist/src/gateway.js +90 -107
- package/dist/src/outbound.js +17 -18
- package/dist/src/ref-index-store.d.ts +1 -1
- package/dist/src/ref-index-store.js +2 -3
- package/dist/src/slash-commands.js +251 -21
- package/dist/src/tools/channel.d.ts +16 -0
- package/dist/src/tools/channel.js +234 -0
- package/dist/src/types.d.ts +7 -1
- package/dist/src/update-checker.d.ts +4 -9
- package/dist/src/update-checker.js +63 -46
- package/dist/src/user-messages.d.ts +6 -14
- package/dist/src/user-messages.js +6 -18
- package/package.json +1 -1
- package/scripts/test-sendmedia.ts +116 -0
- package/scripts/upgrade-via-npm.sh +29 -7
- package/src/channel.ts +12 -33
- package/src/gateway.ts +83 -99
- package/src/outbound.ts +17 -18
- package/src/ref-index-store.ts +3 -4
- package/src/slash-commands.ts +273 -21
- package/src/types.ts +7 -1
- package/src/update-checker.ts +60 -55
- package/src/user-messages.ts +5 -22
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**Connect your AI assistant to QQ — private chat, group chat, and rich media, all in one plugin.**
|
|
12
12
|
|
|
13
|
-
### 🚀 Current Version: `v1.6.
|
|
13
|
+
### 🚀 Current Version: `v1.6.2`
|
|
14
14
|
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](https://bot.q.qq.com/wiki/)
|
|
@@ -149,9 +149,9 @@ AI can send videos directly. Supports local files and URLs.
|
|
|
149
149
|
|
|
150
150
|
The plugin provides built-in slash commands that are intercepted before reaching the AI queue, giving instant responses for diagnostics and management.
|
|
151
151
|
|
|
152
|
-
#### `/
|
|
152
|
+
#### `/bot-ping` — Latency Test
|
|
153
153
|
|
|
154
|
-
> **You**: `/
|
|
154
|
+
> **You**: `/bot-ping`
|
|
155
155
|
>
|
|
156
156
|
> **QQBot**: ✅ pong!⏱ Latency: 602ms (network: 602ms, plugin: 0ms)
|
|
157
157
|
|
|
@@ -159,27 +159,27 @@ Measures end-to-end latency from QQ server push to plugin response, broken down
|
|
|
159
159
|
|
|
160
160
|
<img width="360" src="docs/images/slash-ping.jpg" alt="Ping Demo" />
|
|
161
161
|
|
|
162
|
-
#### `/
|
|
162
|
+
#### `/bot-version` — Version Info
|
|
163
163
|
|
|
164
|
-
> **You**: `/
|
|
164
|
+
> **You**: `/bot-version`
|
|
165
165
|
>
|
|
166
|
-
> **QQBot**: 🦞 Framework: OpenClaw 2026.3.13 (61d171a) / 🤖 Plugin: v1.6.
|
|
166
|
+
> **QQBot**: 🦞 Framework: OpenClaw 2026.3.13 (61d171a) / 🤖 Plugin: v1.6.3 / 🌟 GitHub repo
|
|
167
167
|
|
|
168
168
|
Shows framework version, plugin version, and a direct link to the official repository.
|
|
169
169
|
|
|
170
170
|
<img width="360" src="docs/images/slash-version.jpg" alt="Version Demo" />
|
|
171
171
|
|
|
172
|
-
#### `/
|
|
172
|
+
#### `/bot-help` — Command List
|
|
173
173
|
|
|
174
|
-
> **You**: `/
|
|
174
|
+
> **You**: `/bot-help`
|
|
175
175
|
>
|
|
176
176
|
> **QQBot**: Lists all available slash commands with clickable shortcuts.
|
|
177
177
|
|
|
178
178
|
<img width="360" src="docs/images/slash-help.jpg" alt="Help Demo" />
|
|
179
179
|
|
|
180
|
-
#### `/
|
|
180
|
+
#### `/bot-upgrade` — Upgrade Guide
|
|
181
181
|
|
|
182
|
-
> **You**: `/
|
|
182
|
+
> **You**: `/bot-upgrade`
|
|
183
183
|
>
|
|
184
184
|
> **QQBot**: 📌 Current version / ✅ Up to date / ⬆️ Upgrade guide / 🌟 GitHub repo
|
|
185
185
|
|
|
@@ -187,9 +187,9 @@ Shows current version, update status, upgrade guide link, and official repositor
|
|
|
187
187
|
|
|
188
188
|
<img width="360" src="docs/images/slash-upgrade.jpg" alt="Upgrade Demo" />
|
|
189
189
|
|
|
190
|
-
#### `/
|
|
190
|
+
#### `/bot-logs` — Log Export
|
|
191
191
|
|
|
192
|
-
> **You**: `/
|
|
192
|
+
> **You**: `/bot-logs`
|
|
193
193
|
>
|
|
194
194
|
> **QQBot**: 📋 Logs packaged (~2000 lines), sending file... *(sends a .txt file)*
|
|
195
195
|
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
**让你的 AI 助手接入 QQ — 私聊、群聊、富媒体,一个插件全搞定。**
|
|
11
11
|
|
|
12
|
-
### 🚀 当前版本: `v1.6.
|
|
12
|
+
### 🚀 当前版本: `v1.6.2`
|
|
13
13
|
|
|
14
14
|
[](./LICENSE)
|
|
15
15
|
[](https://bot.q.qq.com/wiki/)
|
|
@@ -144,9 +144,9 @@ AI 可直接发送视频,支持本地文件和公网 URL。
|
|
|
144
144
|
|
|
145
145
|
插件内置一组斜杠指令,在消息进入 AI 队列前拦截处理,即时响应,用于诊断和管理。
|
|
146
146
|
|
|
147
|
-
#### `/
|
|
147
|
+
#### `/bot-ping` — 延迟测试
|
|
148
148
|
|
|
149
|
-
> **你**:`/
|
|
149
|
+
> **你**:`/bot-ping`
|
|
150
150
|
>
|
|
151
151
|
> **QQBot**:✅ pong!⏱ 延迟: 602ms(网络传输: 602ms,插件处理: 0ms)
|
|
152
152
|
|
|
@@ -154,27 +154,27 @@ AI 可直接发送视频,支持本地文件和公网 URL。
|
|
|
154
154
|
|
|
155
155
|
<img width="360" src="docs/images/slash-ping.jpg" alt="Ping 演示" />
|
|
156
156
|
|
|
157
|
-
#### `/
|
|
157
|
+
#### `/bot-version` — 版本信息
|
|
158
158
|
|
|
159
|
-
> **你**:`/
|
|
159
|
+
> **你**:`/bot-version`
|
|
160
160
|
>
|
|
161
|
-
> **QQBot**:🦞框架版本:OpenClaw 2026.3.13 (61d171a) / 🤖QQBot 插件版本:v1.6.
|
|
161
|
+
> **QQBot**:🦞框架版本:OpenClaw 2026.3.13 (61d171a) / 🤖QQBot 插件版本:v1.6.3 / 🌟官方 GitHub 仓库
|
|
162
162
|
|
|
163
163
|
一目了然查看框架版本、插件版本,并可直接跳转官方仓库。
|
|
164
164
|
|
|
165
165
|
<img width="360" src="docs/images/slash-version.jpg" alt="Version 演示" />
|
|
166
166
|
|
|
167
|
-
#### `/
|
|
167
|
+
#### `/bot-help` — 指令列表
|
|
168
168
|
|
|
169
|
-
> **你**:`/
|
|
169
|
+
> **你**:`/bot-help`
|
|
170
170
|
>
|
|
171
171
|
> **QQBot**:列出所有可用的斜杠指令及说明,指令可点击快速输入。
|
|
172
172
|
|
|
173
173
|
<img width="360" src="docs/images/slash-help.jpg" alt="Help 演示" />
|
|
174
174
|
|
|
175
|
-
#### `/
|
|
175
|
+
#### `/bot-upgrade` — 升级指引
|
|
176
176
|
|
|
177
|
-
> **你**:`/
|
|
177
|
+
> **你**:`/bot-upgrade`
|
|
178
178
|
>
|
|
179
179
|
> **QQBot**:📌当前版本 / ✅当前已是最新版本 / ⬆️升级指引 / 🌟官方 GitHub 仓库
|
|
180
180
|
|
|
@@ -182,9 +182,9 @@ AI 可直接发送视频,支持本地文件和公网 URL。
|
|
|
182
182
|
|
|
183
183
|
<img width="360" src="docs/images/slash-upgrade.jpg" alt="Upgrade 演示" />
|
|
184
184
|
|
|
185
|
-
#### `/
|
|
185
|
+
#### `/bot-logs` — 日志导出
|
|
186
186
|
|
|
187
|
-
> **你**:`/
|
|
187
|
+
> **你**:`/bot-logs`
|
|
188
188
|
>
|
|
189
189
|
> **QQBot**:📋 日志已打包(约 2000 行),正在发送文件… *(发送 .txt 文件)*
|
|
190
190
|
|
package/dist/src/channel.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { type ChannelPlugin } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { ResolvedQQBotAccount } from "./types.js";
|
|
3
|
+
/** QQ Bot 单条消息文本长度上限 */
|
|
4
|
+
export declare const TEXT_CHUNK_LIMIT = 5000;
|
|
5
|
+
/**
|
|
6
|
+
* Markdown 感知的文本分块函数
|
|
7
|
+
* 委托给 SDK 内置的 channel.text.chunkMarkdownText
|
|
8
|
+
* 支持代码块自动关闭/重开、括号感知等
|
|
9
|
+
*/
|
|
10
|
+
export declare function chunkText(text: string, limit: number): string[];
|
|
3
11
|
export declare const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount>;
|
package/dist/src/channel.js
CHANGED
|
@@ -4,34 +4,16 @@ import { sendText, sendMedia } from "./outbound.js";
|
|
|
4
4
|
import { startGateway } from "./gateway.js";
|
|
5
5
|
import { qqbotOnboardingAdapter } from "./onboarding.js";
|
|
6
6
|
import { getQQBotRuntime } from "./runtime.js";
|
|
7
|
+
/** QQ Bot 单条消息文本长度上限 */
|
|
8
|
+
export const TEXT_CHUNK_LIMIT = 5000;
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Markdown 感知的文本分块函数
|
|
11
|
+
* 委托给 SDK 内置的 channel.text.chunkMarkdownText
|
|
12
|
+
* 支持代码块自动关闭/重开、括号感知等
|
|
10
13
|
*/
|
|
11
|
-
function chunkText(text, limit) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const chunks = [];
|
|
15
|
-
let remaining = text;
|
|
16
|
-
while (remaining.length > 0) {
|
|
17
|
-
if (remaining.length <= limit) {
|
|
18
|
-
chunks.push(remaining);
|
|
19
|
-
break;
|
|
20
|
-
}
|
|
21
|
-
// 尝试在换行处分割
|
|
22
|
-
let splitAt = remaining.lastIndexOf("\n", limit);
|
|
23
|
-
if (splitAt <= 0 || splitAt < limit * 0.5) {
|
|
24
|
-
// 没找到合适的换行,尝试在空格处分割
|
|
25
|
-
splitAt = remaining.lastIndexOf(" ", limit);
|
|
26
|
-
}
|
|
27
|
-
if (splitAt <= 0 || splitAt < limit * 0.5) {
|
|
28
|
-
// 还是没找到,强制在 limit 处分割
|
|
29
|
-
splitAt = limit;
|
|
30
|
-
}
|
|
31
|
-
chunks.push(remaining.slice(0, splitAt));
|
|
32
|
-
remaining = remaining.slice(splitAt).trimStart();
|
|
33
|
-
}
|
|
34
|
-
return chunks;
|
|
14
|
+
export function chunkText(text, limit) {
|
|
15
|
+
const runtime = getQQBotRuntime();
|
|
16
|
+
return runtime.channel.text.chunkMarkdownText(text, limit);
|
|
35
17
|
}
|
|
36
18
|
export const qqbotPlugin = {
|
|
37
19
|
id: "qqbot",
|
|
@@ -52,7 +34,7 @@ export const qqbotPlugin = {
|
|
|
52
34
|
* blockStreaming: true 表示该 Channel 支持块流式
|
|
53
35
|
* 框架会收集流式响应,然后通过 deliver 回调发送
|
|
54
36
|
*/
|
|
55
|
-
blockStreaming:
|
|
37
|
+
blockStreaming: true,
|
|
56
38
|
},
|
|
57
39
|
reload: { configPrefixes: ["channels.qqbot"] },
|
|
58
40
|
// CLI onboarding wizard
|
|
@@ -205,9 +187,9 @@ export const qqbotPlugin = {
|
|
|
205
187
|
},
|
|
206
188
|
outbound: {
|
|
207
189
|
deliveryMode: "direct",
|
|
208
|
-
chunker:
|
|
190
|
+
chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
209
191
|
chunkerMode: "markdown",
|
|
210
|
-
textChunkLimit:
|
|
192
|
+
textChunkLimit: 5000,
|
|
211
193
|
sendText: async ({ to, text, accountId, replyToId, cfg }) => {
|
|
212
194
|
console.log(`[qqbot:channel] sendText called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, text.length=${text?.length ?? 0}`);
|
|
213
195
|
console.log(`[qqbot:channel] sendText text preview: ${text?.slice(0, 100)}${(text?.length ?? 0) > 100 ? "..." : ""}`);
|
package/dist/src/gateway.js
CHANGED
|
@@ -7,7 +7,7 @@ import { recordKnownUser, flushKnownUsers, listKnownUsers } from "./known-users.
|
|
|
7
7
|
import { getQQBotRuntime } from "./runtime.js";
|
|
8
8
|
import { setRefIndex, getRefIndex, formatRefEntryForAgent, flushRefIndex } from "./ref-index-store.js";
|
|
9
9
|
import { matchSlashCommand, getPluginVersion } from "./slash-commands.js";
|
|
10
|
-
import { triggerUpdateCheck
|
|
10
|
+
import { triggerUpdateCheck } from "./update-checker.js";
|
|
11
11
|
import { startImageServer, isImageServerRunning, downloadFile } from "./image-server.js";
|
|
12
12
|
import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize } from "./utils/image-size.js";
|
|
13
13
|
import { parseQQBotPayload, encodePayloadForCron, isCronReminderPayload, isMediaPayload } from "./utils/payload.js";
|
|
@@ -15,8 +15,8 @@ import { convertSilkToWav, isVoiceAttachment, formatDuration, resolveTTSConfig,
|
|
|
15
15
|
import { normalizeMediaTags } from "./utils/media-tags.js";
|
|
16
16
|
import { checkFileSize, readFileAsync, fileExistsAsync, formatFileSize } from "./utils/file-utils.js";
|
|
17
17
|
import { getQQBotDataDir, isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, runDiagnostics } from "./utils/platform.js";
|
|
18
|
-
import { MSG } from "./user-messages.js";
|
|
19
18
|
import { sendPhoto, sendVoice, sendVideoMsg, sendDocument, sendMedia as sendMediaAuto } from "./outbound.js";
|
|
19
|
+
import { chunkText, TEXT_CHUNK_LIMIT } from "./channel.js";
|
|
20
20
|
function resolveSTTConfig(cfg) {
|
|
21
21
|
const c = cfg;
|
|
22
22
|
// 优先使用 channels.qqbot.stt(插件专属配置)
|
|
@@ -172,6 +172,7 @@ function parseFaceTags(text) {
|
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
|
+
// formatMediaErrorMessage 已移至 user-messages.ts 集中管理
|
|
175
176
|
// ============ 内部标记过滤 ============
|
|
176
177
|
/**
|
|
177
178
|
* 过滤内部标记(如 [[reply_to: xxx]])
|
|
@@ -314,36 +315,8 @@ export async function startGateway(ctx) {
|
|
|
314
315
|
log?.info(`[qqbot:${account.accountId}] ${w}`);
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
|
-
//
|
|
318
|
+
// 后台版本检查(供 /bot-version、/bot-upgrade 指令被动查询)
|
|
318
319
|
triggerUpdateCheck(log);
|
|
319
|
-
// 注册新版本通知回调:仅发给管理员,带防抖
|
|
320
|
-
let lastUpdateNotifyAt = 0;
|
|
321
|
-
const UPDATE_NOTIFY_DEBOUNCE_MS = 5 * 60 * 1000; // 5 分钟内不重复通知
|
|
322
|
-
onUpdateFound(async (info) => {
|
|
323
|
-
try {
|
|
324
|
-
// 防抖:避免短时间内重复推送
|
|
325
|
-
const now = Date.now();
|
|
326
|
-
if (now - lastUpdateNotifyAt < UPDATE_NOTIFY_DEBOUNCE_MS) {
|
|
327
|
-
log?.debug?.(`[qqbot:${account.accountId}] Update notification debounced`);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const notice = formatUpdateNotice(info);
|
|
331
|
-
if (!notice)
|
|
332
|
-
return;
|
|
333
|
-
const adminId = resolveAdminOpenId();
|
|
334
|
-
if (!adminId) {
|
|
335
|
-
log?.debug?.(`[qqbot:${account.accountId}] No admin or known user to send update notification`);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
339
|
-
await sendProactiveC2CMessage(token, adminId, notice);
|
|
340
|
-
lastUpdateNotifyAt = Date.now();
|
|
341
|
-
log?.info(`[qqbot:${account.accountId}] Sent update notification to admin: ${adminId}`);
|
|
342
|
-
}
|
|
343
|
-
catch (err) {
|
|
344
|
-
log?.debug?.(`[qqbot:${account.accountId}] Failed to send update notification to admin: ${err}`);
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
320
|
// 初始化 API 配置(markdown 支持)
|
|
348
321
|
initApiConfig({
|
|
349
322
|
markdownSupport: account.markdownSupport,
|
|
@@ -373,7 +346,7 @@ export async function startGateway(ctx) {
|
|
|
373
346
|
attachments.push(attachment);
|
|
374
347
|
}
|
|
375
348
|
setRefIndex(refIdx, {
|
|
376
|
-
content:
|
|
349
|
+
content: meta.text ?? "",
|
|
377
350
|
senderId: account.accountId,
|
|
378
351
|
senderName: account.accountId,
|
|
379
352
|
timestamp: Date.now(),
|
|
@@ -465,21 +438,23 @@ export async function startGateway(ctx) {
|
|
|
465
438
|
const greeting = getStartupGreeting();
|
|
466
439
|
if (!greeting) {
|
|
467
440
|
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (debounced, trigger=${trigger})`);
|
|
468
|
-
return;
|
|
469
441
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
442
|
+
else {
|
|
443
|
+
const adminId = resolveAdminOpenId();
|
|
444
|
+
if (!adminId) {
|
|
445
|
+
log?.info(`[qqbot:${account.accountId}] Skipping startup greeting (no admin or known user)`);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
log?.info(`[qqbot:${account.accountId}] Sending startup greeting to admin (trigger=${trigger}): "${greeting}"`);
|
|
449
|
+
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
450
|
+
const GREETING_TIMEOUT_MS = 10_000;
|
|
451
|
+
await Promise.race([
|
|
452
|
+
sendProactiveC2CMessage(token, adminId, greeting),
|
|
453
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
454
|
+
]);
|
|
455
|
+
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to admin: ${adminId}`);
|
|
456
|
+
}
|
|
474
457
|
}
|
|
475
|
-
log?.info(`[qqbot:${account.accountId}] Sending startup greeting to admin (trigger=${trigger}): "${greeting}"`);
|
|
476
|
-
const token = await getAccessToken(account.appId, account.clientSecret);
|
|
477
|
-
const GREETING_TIMEOUT_MS = 10_000;
|
|
478
|
-
await Promise.race([
|
|
479
|
-
sendProactiveC2CMessage(token, adminId, greeting),
|
|
480
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Startup greeting send timeout (10s)")), GREETING_TIMEOUT_MS)),
|
|
481
|
-
]);
|
|
482
|
-
log?.info(`[qqbot:${account.accountId}] Sent startup greeting to admin: ${adminId}`);
|
|
483
458
|
}
|
|
484
459
|
catch (err) {
|
|
485
460
|
log?.error(`[qqbot:${account.accountId}] Failed to send startup greeting: ${err}`);
|
|
@@ -1506,23 +1481,27 @@ export async function startGateway(ctx) {
|
|
|
1506
1481
|
};
|
|
1507
1482
|
for (const item of sendQueue) {
|
|
1508
1483
|
if (item.type === "text") {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1484
|
+
// 对长文本进行分块发送
|
|
1485
|
+
const textChunks = getQQBotRuntime().channel.text.chunkMarkdownText(item.content, TEXT_CHUNK_LIMIT);
|
|
1486
|
+
for (const chunk of textChunks) {
|
|
1487
|
+
try {
|
|
1488
|
+
await sendWithTokenRetry(async (token) => {
|
|
1489
|
+
const ref = consumeQuoteRef();
|
|
1490
|
+
if (event.type === "c2c") {
|
|
1491
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
1492
|
+
}
|
|
1493
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
1494
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
1495
|
+
}
|
|
1496
|
+
else if (event.channelId) {
|
|
1497
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${item.content.length} chars): ${chunk.slice(0, 50)}...`);
|
|
1501
|
+
}
|
|
1502
|
+
catch (err) {
|
|
1503
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send text chunk: ${err}`);
|
|
1504
|
+
}
|
|
1526
1505
|
}
|
|
1527
1506
|
}
|
|
1528
1507
|
else if (item.type === "image") {
|
|
@@ -1589,9 +1568,7 @@ export async function startGateway(ctx) {
|
|
|
1589
1568
|
const payloadResult = parseQQBotPayload(replyText);
|
|
1590
1569
|
if (payloadResult.isPayload) {
|
|
1591
1570
|
if (payloadResult.error) {
|
|
1592
|
-
// 载荷解析失败,发送错误提示
|
|
1593
1571
|
log?.error(`[qqbot:${account.accountId}] Payload parse error: ${payloadResult.error}`);
|
|
1594
|
-
await sendErrorMessage(MSG.PAYLOAD_PARSE_ERROR);
|
|
1595
1572
|
return;
|
|
1596
1573
|
}
|
|
1597
1574
|
if (payloadResult.payload) {
|
|
@@ -1662,7 +1639,7 @@ export async function startGateway(ctx) {
|
|
|
1662
1639
|
};
|
|
1663
1640
|
const mimeType = mimeTypes[ext];
|
|
1664
1641
|
if (!mimeType) {
|
|
1665
|
-
|
|
1642
|
+
log?.error(`[qqbot:${account.accountId}] Unsupported image format: ${ext}`);
|
|
1666
1643
|
return;
|
|
1667
1644
|
}
|
|
1668
1645
|
imageUrl = `data:${mimeType};base64,${base64Data}`;
|
|
@@ -1712,7 +1689,7 @@ export async function startGateway(ctx) {
|
|
|
1712
1689
|
try {
|
|
1713
1690
|
const ttsText = parsedPayload.caption || parsedPayload.path;
|
|
1714
1691
|
if (!ttsText?.trim()) {
|
|
1715
|
-
|
|
1692
|
+
log?.error(`[qqbot:${account.accountId}] Voice missing text`);
|
|
1716
1693
|
}
|
|
1717
1694
|
else {
|
|
1718
1695
|
const ttsCfg = resolveTTSConfig(cfg);
|
|
@@ -1732,7 +1709,8 @@ export async function startGateway(ctx) {
|
|
|
1732
1709
|
await sendGroupVoiceMessage(token, event.groupOpenid, silkBase64, event.messageId);
|
|
1733
1710
|
}
|
|
1734
1711
|
else if (event.channelId) {
|
|
1735
|
-
|
|
1712
|
+
log?.error(`[qqbot:${account.accountId}] Voice not supported in channel, sending text fallback`);
|
|
1713
|
+
await sendChannelMessage(token, event.channelId, ttsText, event.messageId);
|
|
1736
1714
|
}
|
|
1737
1715
|
});
|
|
1738
1716
|
log?.info(`[qqbot:${account.accountId}] Voice message sent`);
|
|
@@ -1748,7 +1726,7 @@ export async function startGateway(ctx) {
|
|
|
1748
1726
|
try {
|
|
1749
1727
|
const videoPath = normalizePath(parsedPayload.path ?? "");
|
|
1750
1728
|
if (!videoPath?.trim()) {
|
|
1751
|
-
|
|
1729
|
+
log?.error(`[qqbot:${account.accountId}] Video missing path`);
|
|
1752
1730
|
}
|
|
1753
1731
|
else {
|
|
1754
1732
|
const isHttpUrl = videoPath.startsWith("http://") || videoPath.startsWith("https://");
|
|
@@ -1763,7 +1741,7 @@ export async function startGateway(ctx) {
|
|
|
1763
1741
|
await sendGroupVideoMessage(token, event.groupOpenid, videoPath, undefined, event.messageId);
|
|
1764
1742
|
}
|
|
1765
1743
|
else if (event.channelId) {
|
|
1766
|
-
|
|
1744
|
+
log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
|
|
1767
1745
|
}
|
|
1768
1746
|
}
|
|
1769
1747
|
else {
|
|
@@ -1785,7 +1763,7 @@ export async function startGateway(ctx) {
|
|
|
1785
1763
|
await sendGroupVideoMessage(token, event.groupOpenid, undefined, videoBase64, event.messageId);
|
|
1786
1764
|
}
|
|
1787
1765
|
else if (event.channelId) {
|
|
1788
|
-
|
|
1766
|
+
log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
|
|
1789
1767
|
}
|
|
1790
1768
|
}
|
|
1791
1769
|
});
|
|
@@ -1815,7 +1793,7 @@ export async function startGateway(ctx) {
|
|
|
1815
1793
|
try {
|
|
1816
1794
|
const filePath = normalizePath(parsedPayload.path ?? "");
|
|
1817
1795
|
if (!filePath?.trim()) {
|
|
1818
|
-
|
|
1796
|
+
log?.error(`[qqbot:${account.accountId}] File missing path`);
|
|
1819
1797
|
}
|
|
1820
1798
|
else {
|
|
1821
1799
|
const isHttpUrl = filePath.startsWith("http://") || filePath.startsWith("https://");
|
|
@@ -1830,7 +1808,7 @@ export async function startGateway(ctx) {
|
|
|
1830
1808
|
await sendGroupFileMessage(token, event.groupOpenid, undefined, filePath, event.messageId, fileName);
|
|
1831
1809
|
}
|
|
1832
1810
|
else if (event.channelId) {
|
|
1833
|
-
|
|
1811
|
+
log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
|
|
1834
1812
|
}
|
|
1835
1813
|
}
|
|
1836
1814
|
else {
|
|
@@ -1850,7 +1828,7 @@ export async function startGateway(ctx) {
|
|
|
1850
1828
|
await sendGroupFileMessage(token, event.groupOpenid, fileBase64, undefined, event.messageId, fileName);
|
|
1851
1829
|
}
|
|
1852
1830
|
else if (event.channelId) {
|
|
1853
|
-
|
|
1831
|
+
log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
|
|
1854
1832
|
}
|
|
1855
1833
|
}
|
|
1856
1834
|
});
|
|
@@ -1863,7 +1841,6 @@ export async function startGateway(ctx) {
|
|
|
1863
1841
|
}
|
|
1864
1842
|
else {
|
|
1865
1843
|
log?.error(`[qqbot:${account.accountId}] Unknown media type: ${parsedPayload.mediaType}`);
|
|
1866
|
-
await sendErrorMessage(MSG.UNSUPPORTED_MEDIA_TYPE);
|
|
1867
1844
|
}
|
|
1868
1845
|
// 记录活动并返回
|
|
1869
1846
|
pluginRuntime.channel.activity.record({
|
|
@@ -1876,7 +1853,6 @@ export async function startGateway(ctx) {
|
|
|
1876
1853
|
else {
|
|
1877
1854
|
// 未知的载荷类型
|
|
1878
1855
|
log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${parsedPayload.type}`);
|
|
1879
|
-
await sendErrorMessage(MSG.UNSUPPORTED_PAYLOAD_TYPE);
|
|
1880
1856
|
return;
|
|
1881
1857
|
}
|
|
1882
1858
|
}
|
|
@@ -2064,23 +2040,26 @@ export async function startGateway(ctx) {
|
|
|
2064
2040
|
}
|
|
2065
2041
|
// 🔹 第三步:发送带公网图片的 markdown 消息
|
|
2066
2042
|
if (textWithoutImages.trim()) {
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2043
|
+
const mdChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
|
|
2044
|
+
for (const chunk of mdChunks) {
|
|
2045
|
+
try {
|
|
2046
|
+
await sendWithTokenRetry(async (token) => {
|
|
2047
|
+
const ref = consumeQuoteRef();
|
|
2048
|
+
if (event.type === "c2c") {
|
|
2049
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
2050
|
+
}
|
|
2051
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
2052
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
2053
|
+
}
|
|
2054
|
+
else if (event.channelId) {
|
|
2055
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
log?.info(`[qqbot:${account.accountId}] Sent markdown chunk (${chunk.length}/${textWithoutImages.length} chars) with ${httpImageUrls.length} HTTP images (${event.type})`);
|
|
2059
|
+
}
|
|
2060
|
+
catch (err) {
|
|
2061
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send markdown message chunk: ${err}`);
|
|
2062
|
+
}
|
|
2084
2063
|
}
|
|
2085
2064
|
}
|
|
2086
2065
|
}
|
|
@@ -2120,21 +2099,24 @@ export async function startGateway(ctx) {
|
|
|
2120
2099
|
log?.error(`[qqbot:${account.accountId}] Failed to send image: ${imgErr}`);
|
|
2121
2100
|
}
|
|
2122
2101
|
}
|
|
2123
|
-
//
|
|
2102
|
+
// 发送文本消息(分块)
|
|
2124
2103
|
if (textWithoutImages.trim()) {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2104
|
+
const plainChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
|
|
2105
|
+
for (const chunk of plainChunks) {
|
|
2106
|
+
await sendWithTokenRetry(async (token) => {
|
|
2107
|
+
const ref = consumeQuoteRef();
|
|
2108
|
+
if (event.type === "c2c") {
|
|
2109
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
2110
|
+
}
|
|
2111
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
2112
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
2113
|
+
}
|
|
2114
|
+
else if (event.channelId) {
|
|
2115
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${textWithoutImages.length} chars) (${event.type})`);
|
|
2119
|
+
}
|
|
2138
2120
|
}
|
|
2139
2121
|
}
|
|
2140
2122
|
catch (err) {
|
|
@@ -2219,7 +2201,7 @@ export async function startGateway(ctx) {
|
|
|
2219
2201
|
},
|
|
2220
2202
|
},
|
|
2221
2203
|
replyOptions: {
|
|
2222
|
-
disableBlockStreaming:
|
|
2204
|
+
disableBlockStreaming: true,
|
|
2223
2205
|
},
|
|
2224
2206
|
});
|
|
2225
2207
|
// 等待分发完成或超时
|
|
@@ -2352,6 +2334,7 @@ export async function startGateway(ctx) {
|
|
|
2352
2334
|
}
|
|
2353
2335
|
else if (t === "RESUMED") {
|
|
2354
2336
|
log?.info(`[qqbot:${account.accountId}] Session resumed`);
|
|
2337
|
+
onReady?.(d); // 通知框架连接已恢复,避免 health-monitor 误判 disconnected
|
|
2355
2338
|
// RESUMED 也属于首次启动(gateway restart 通常走 resume)
|
|
2356
2339
|
if (isFirstReadyGlobal) {
|
|
2357
2340
|
isFirstReadyGlobal = false;
|