@tencent-connect/openclaw-qqbot 1.6.2-alpha.2 → 1.6.2-alpha.3
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 +2 -2
- package/README.zh.md +2 -2
- package/dist/src/channel.d.ts +8 -0
- package/dist/src/channel.js +11 -29
- package/dist/src/gateway.js +75 -94
- 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/update-checker.d.ts +0 -7
- package/dist/src/update-checker.js +0 -19
- package/dist/src/user-messages.d.ts +6 -14
- package/dist/src/user-messages.js +6 -18
- package/package.json +1 -1
- package/src/channel.ts +12 -33
- package/src/gateway.ts +69 -85
- package/src/outbound.ts +17 -18
- package/src/ref-index-store.ts +3 -4
- package/src/update-checker.ts +0 -23
- 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/)
|
|
@@ -163,7 +163,7 @@ Measures end-to-end latency from QQ server push to plugin response, broken down
|
|
|
163
163
|
|
|
164
164
|
> **You**: `/qqbot-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.2 / 🌟 GitHub repo
|
|
167
167
|
|
|
168
168
|
Shows framework version, plugin version, and a direct link to the official repository.
|
|
169
169
|
|
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/)
|
|
@@ -158,7 +158,7 @@ AI 可直接发送视频,支持本地文件和公网 URL。
|
|
|
158
158
|
|
|
159
159
|
> **你**:`/qqbot-version`
|
|
160
160
|
>
|
|
161
|
-
> **QQBot**:🦞框架版本:OpenClaw 2026.3.13 (61d171a) / 🤖QQBot 插件版本:v1.6.
|
|
161
|
+
> **QQBot**:🦞框架版本:OpenClaw 2026.3.13 (61d171a) / 🤖QQBot 插件版本:v1.6.2 / 🌟官方 GitHub 仓库
|
|
162
162
|
|
|
163
163
|
一目了然查看框架版本、插件版本,并可直接跳转官方仓库。
|
|
164
164
|
|
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
|
+
// 后台版本检查(供 /qqbot-version、/qqbot-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(),
|
|
@@ -1506,23 +1479,27 @@ export async function startGateway(ctx) {
|
|
|
1506
1479
|
};
|
|
1507
1480
|
for (const item of sendQueue) {
|
|
1508
1481
|
if (item.type === "text") {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1482
|
+
// 对长文本进行分块发送
|
|
1483
|
+
const textChunks = getQQBotRuntime().channel.text.chunkMarkdownText(item.content, TEXT_CHUNK_LIMIT);
|
|
1484
|
+
for (const chunk of textChunks) {
|
|
1485
|
+
try {
|
|
1486
|
+
await sendWithTokenRetry(async (token) => {
|
|
1487
|
+
const ref = consumeQuoteRef();
|
|
1488
|
+
if (event.type === "c2c") {
|
|
1489
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
1490
|
+
}
|
|
1491
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
1492
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
1493
|
+
}
|
|
1494
|
+
else if (event.channelId) {
|
|
1495
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${item.content.length} chars): ${chunk.slice(0, 50)}...`);
|
|
1499
|
+
}
|
|
1500
|
+
catch (err) {
|
|
1501
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send text chunk: ${err}`);
|
|
1502
|
+
}
|
|
1526
1503
|
}
|
|
1527
1504
|
}
|
|
1528
1505
|
else if (item.type === "image") {
|
|
@@ -1589,9 +1566,7 @@ export async function startGateway(ctx) {
|
|
|
1589
1566
|
const payloadResult = parseQQBotPayload(replyText);
|
|
1590
1567
|
if (payloadResult.isPayload) {
|
|
1591
1568
|
if (payloadResult.error) {
|
|
1592
|
-
// 载荷解析失败,发送错误提示
|
|
1593
1569
|
log?.error(`[qqbot:${account.accountId}] Payload parse error: ${payloadResult.error}`);
|
|
1594
|
-
await sendErrorMessage(MSG.PAYLOAD_PARSE_ERROR);
|
|
1595
1570
|
return;
|
|
1596
1571
|
}
|
|
1597
1572
|
if (payloadResult.payload) {
|
|
@@ -1662,7 +1637,7 @@ export async function startGateway(ctx) {
|
|
|
1662
1637
|
};
|
|
1663
1638
|
const mimeType = mimeTypes[ext];
|
|
1664
1639
|
if (!mimeType) {
|
|
1665
|
-
|
|
1640
|
+
log?.error(`[qqbot:${account.accountId}] Unsupported image format: ${ext}`);
|
|
1666
1641
|
return;
|
|
1667
1642
|
}
|
|
1668
1643
|
imageUrl = `data:${mimeType};base64,${base64Data}`;
|
|
@@ -1712,7 +1687,7 @@ export async function startGateway(ctx) {
|
|
|
1712
1687
|
try {
|
|
1713
1688
|
const ttsText = parsedPayload.caption || parsedPayload.path;
|
|
1714
1689
|
if (!ttsText?.trim()) {
|
|
1715
|
-
|
|
1690
|
+
log?.error(`[qqbot:${account.accountId}] Voice missing text`);
|
|
1716
1691
|
}
|
|
1717
1692
|
else {
|
|
1718
1693
|
const ttsCfg = resolveTTSConfig(cfg);
|
|
@@ -1732,7 +1707,8 @@ export async function startGateway(ctx) {
|
|
|
1732
1707
|
await sendGroupVoiceMessage(token, event.groupOpenid, silkBase64, event.messageId);
|
|
1733
1708
|
}
|
|
1734
1709
|
else if (event.channelId) {
|
|
1735
|
-
|
|
1710
|
+
log?.error(`[qqbot:${account.accountId}] Voice not supported in channel, sending text fallback`);
|
|
1711
|
+
await sendChannelMessage(token, event.channelId, ttsText, event.messageId);
|
|
1736
1712
|
}
|
|
1737
1713
|
});
|
|
1738
1714
|
log?.info(`[qqbot:${account.accountId}] Voice message sent`);
|
|
@@ -1748,7 +1724,7 @@ export async function startGateway(ctx) {
|
|
|
1748
1724
|
try {
|
|
1749
1725
|
const videoPath = normalizePath(parsedPayload.path ?? "");
|
|
1750
1726
|
if (!videoPath?.trim()) {
|
|
1751
|
-
|
|
1727
|
+
log?.error(`[qqbot:${account.accountId}] Video missing path`);
|
|
1752
1728
|
}
|
|
1753
1729
|
else {
|
|
1754
1730
|
const isHttpUrl = videoPath.startsWith("http://") || videoPath.startsWith("https://");
|
|
@@ -1763,7 +1739,7 @@ export async function startGateway(ctx) {
|
|
|
1763
1739
|
await sendGroupVideoMessage(token, event.groupOpenid, videoPath, undefined, event.messageId);
|
|
1764
1740
|
}
|
|
1765
1741
|
else if (event.channelId) {
|
|
1766
|
-
|
|
1742
|
+
log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
|
|
1767
1743
|
}
|
|
1768
1744
|
}
|
|
1769
1745
|
else {
|
|
@@ -1785,7 +1761,7 @@ export async function startGateway(ctx) {
|
|
|
1785
1761
|
await sendGroupVideoMessage(token, event.groupOpenid, undefined, videoBase64, event.messageId);
|
|
1786
1762
|
}
|
|
1787
1763
|
else if (event.channelId) {
|
|
1788
|
-
|
|
1764
|
+
log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
|
|
1789
1765
|
}
|
|
1790
1766
|
}
|
|
1791
1767
|
});
|
|
@@ -1815,7 +1791,7 @@ export async function startGateway(ctx) {
|
|
|
1815
1791
|
try {
|
|
1816
1792
|
const filePath = normalizePath(parsedPayload.path ?? "");
|
|
1817
1793
|
if (!filePath?.trim()) {
|
|
1818
|
-
|
|
1794
|
+
log?.error(`[qqbot:${account.accountId}] File missing path`);
|
|
1819
1795
|
}
|
|
1820
1796
|
else {
|
|
1821
1797
|
const isHttpUrl = filePath.startsWith("http://") || filePath.startsWith("https://");
|
|
@@ -1830,7 +1806,7 @@ export async function startGateway(ctx) {
|
|
|
1830
1806
|
await sendGroupFileMessage(token, event.groupOpenid, undefined, filePath, event.messageId, fileName);
|
|
1831
1807
|
}
|
|
1832
1808
|
else if (event.channelId) {
|
|
1833
|
-
|
|
1809
|
+
log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
|
|
1834
1810
|
}
|
|
1835
1811
|
}
|
|
1836
1812
|
else {
|
|
@@ -1850,7 +1826,7 @@ export async function startGateway(ctx) {
|
|
|
1850
1826
|
await sendGroupFileMessage(token, event.groupOpenid, fileBase64, undefined, event.messageId, fileName);
|
|
1851
1827
|
}
|
|
1852
1828
|
else if (event.channelId) {
|
|
1853
|
-
|
|
1829
|
+
log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
|
|
1854
1830
|
}
|
|
1855
1831
|
}
|
|
1856
1832
|
});
|
|
@@ -1863,7 +1839,6 @@ export async function startGateway(ctx) {
|
|
|
1863
1839
|
}
|
|
1864
1840
|
else {
|
|
1865
1841
|
log?.error(`[qqbot:${account.accountId}] Unknown media type: ${parsedPayload.mediaType}`);
|
|
1866
|
-
await sendErrorMessage(MSG.UNSUPPORTED_MEDIA_TYPE);
|
|
1867
1842
|
}
|
|
1868
1843
|
// 记录活动并返回
|
|
1869
1844
|
pluginRuntime.channel.activity.record({
|
|
@@ -1876,7 +1851,6 @@ export async function startGateway(ctx) {
|
|
|
1876
1851
|
else {
|
|
1877
1852
|
// 未知的载荷类型
|
|
1878
1853
|
log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${parsedPayload.type}`);
|
|
1879
|
-
await sendErrorMessage(MSG.UNSUPPORTED_PAYLOAD_TYPE);
|
|
1880
1854
|
return;
|
|
1881
1855
|
}
|
|
1882
1856
|
}
|
|
@@ -2064,23 +2038,26 @@ export async function startGateway(ctx) {
|
|
|
2064
2038
|
}
|
|
2065
2039
|
// 🔹 第三步:发送带公网图片的 markdown 消息
|
|
2066
2040
|
if (textWithoutImages.trim()) {
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2041
|
+
const mdChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
|
|
2042
|
+
for (const chunk of mdChunks) {
|
|
2043
|
+
try {
|
|
2044
|
+
await sendWithTokenRetry(async (token) => {
|
|
2045
|
+
const ref = consumeQuoteRef();
|
|
2046
|
+
if (event.type === "c2c") {
|
|
2047
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
2048
|
+
}
|
|
2049
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
2050
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
2051
|
+
}
|
|
2052
|
+
else if (event.channelId) {
|
|
2053
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
log?.info(`[qqbot:${account.accountId}] Sent markdown chunk (${chunk.length}/${textWithoutImages.length} chars) with ${httpImageUrls.length} HTTP images (${event.type})`);
|
|
2057
|
+
}
|
|
2058
|
+
catch (err) {
|
|
2059
|
+
log?.error(`[qqbot:${account.accountId}] Failed to send markdown message chunk: ${err}`);
|
|
2060
|
+
}
|
|
2084
2061
|
}
|
|
2085
2062
|
}
|
|
2086
2063
|
}
|
|
@@ -2120,21 +2097,24 @@ export async function startGateway(ctx) {
|
|
|
2120
2097
|
log?.error(`[qqbot:${account.accountId}] Failed to send image: ${imgErr}`);
|
|
2121
2098
|
}
|
|
2122
2099
|
}
|
|
2123
|
-
//
|
|
2100
|
+
// 发送文本消息(分块)
|
|
2124
2101
|
if (textWithoutImages.trim()) {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2102
|
+
const plainChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
|
|
2103
|
+
for (const chunk of plainChunks) {
|
|
2104
|
+
await sendWithTokenRetry(async (token) => {
|
|
2105
|
+
const ref = consumeQuoteRef();
|
|
2106
|
+
if (event.type === "c2c") {
|
|
2107
|
+
return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
|
|
2108
|
+
}
|
|
2109
|
+
else if (event.type === "group" && event.groupOpenid) {
|
|
2110
|
+
return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
|
|
2111
|
+
}
|
|
2112
|
+
else if (event.channelId) {
|
|
2113
|
+
return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${textWithoutImages.length} chars) (${event.type})`);
|
|
2117
|
+
}
|
|
2138
2118
|
}
|
|
2139
2119
|
}
|
|
2140
2120
|
catch (err) {
|
|
@@ -2219,7 +2199,7 @@ export async function startGateway(ctx) {
|
|
|
2219
2199
|
},
|
|
2220
2200
|
},
|
|
2221
2201
|
replyOptions: {
|
|
2222
|
-
disableBlockStreaming:
|
|
2202
|
+
disableBlockStreaming: true,
|
|
2223
2203
|
},
|
|
2224
2204
|
});
|
|
2225
2205
|
// 等待分发完成或超时
|
|
@@ -2352,6 +2332,7 @@ export async function startGateway(ctx) {
|
|
|
2352
2332
|
}
|
|
2353
2333
|
else if (t === "RESUMED") {
|
|
2354
2334
|
log?.info(`[qqbot:${account.accountId}] Session resumed`);
|
|
2335
|
+
onReady?.(d); // 通知框架连接已恢复,避免 health-monitor 误判 disconnected
|
|
2355
2336
|
// RESUMED 也属于首次启动(gateway restart 通常走 resume)
|
|
2356
2337
|
if (isFirstReadyGlobal) {
|
|
2357
2338
|
isFirstReadyGlobal = false;
|
package/dist/src/outbound.js
CHANGED
|
@@ -9,7 +9,6 @@ import { normalizeMediaTags } from "./utils/media-tags.js";
|
|
|
9
9
|
import { checkFileSize, readFileAsync, fileExistsAsync, formatFileSize } from "./utils/file-utils.js";
|
|
10
10
|
import { isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, getQQBotDataDir } from "./utils/platform.js";
|
|
11
11
|
import { downloadFile } from "./image-server.js";
|
|
12
|
-
import { MSG } from "./user-messages.js";
|
|
13
12
|
// ============ 消息回复限流器 ============
|
|
14
13
|
// 同一 message_id 1小时内最多回复 4 次,超过 1 小时无法被动回复(需改为主动消息)
|
|
15
14
|
const MESSAGE_REPLY_LIMIT = 4;
|
|
@@ -208,7 +207,7 @@ export async function sendPhoto(ctx, imagePath) {
|
|
|
208
207
|
let imageUrl = mediaPath;
|
|
209
208
|
if (isLocal) {
|
|
210
209
|
if (!(await fileExistsAsync(mediaPath))) {
|
|
211
|
-
return { channel: "qqbot", error: "
|
|
210
|
+
return { channel: "qqbot", error: "Image not found" };
|
|
212
211
|
}
|
|
213
212
|
const sizeCheck = checkFileSize(mediaPath);
|
|
214
213
|
if (!sizeCheck.ok) {
|
|
@@ -222,7 +221,7 @@ export async function sendPhoto(ctx, imagePath) {
|
|
|
222
221
|
};
|
|
223
222
|
const mimeType = mimeTypes[ext];
|
|
224
223
|
if (!mimeType) {
|
|
225
|
-
return { channel: "qqbot", error:
|
|
224
|
+
return { channel: "qqbot", error: `Unsupported image format: ${ext}` };
|
|
226
225
|
}
|
|
227
226
|
imageUrl = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
|
|
228
227
|
console.log(`${prefix} sendPhoto: local → Base64 (${formatFileSize(fileBuffer.length)})`);
|
|
@@ -318,8 +317,8 @@ transcodeEnabled = true) {
|
|
|
318
317
|
return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
|
|
319
318
|
}
|
|
320
319
|
else {
|
|
321
|
-
|
|
322
|
-
return { channel: "qqbot",
|
|
320
|
+
console.log(`${prefix} sendVoice: voice not supported in channel`);
|
|
321
|
+
return { channel: "qqbot", error: "Voice not supported in channel" };
|
|
323
322
|
}
|
|
324
323
|
}
|
|
325
324
|
catch (err) {
|
|
@@ -345,7 +344,7 @@ async function sendVoiceFromLocal(ctx, mediaPath, directUploadFormats, transcode
|
|
|
345
344
|
// 等待文件就绪(TTS 异步生成,文件可能还没写完)
|
|
346
345
|
const fileSize = await waitForFile(mediaPath);
|
|
347
346
|
if (fileSize === 0) {
|
|
348
|
-
return { channel: "qqbot", error: "
|
|
347
|
+
return { channel: "qqbot", error: "Voice generate failed" };
|
|
349
348
|
}
|
|
350
349
|
// 精细检测:是否需要转码
|
|
351
350
|
const needsTranscode = shouldTranscodeVoice(mediaPath);
|
|
@@ -376,8 +375,8 @@ async function sendVoiceFromLocal(ctx, mediaPath, directUploadFormats, transcode
|
|
|
376
375
|
return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
|
|
377
376
|
}
|
|
378
377
|
else {
|
|
379
|
-
|
|
380
|
-
return { channel: "qqbot",
|
|
378
|
+
console.log(`${prefix} sendVoice: voice not supported in channel`);
|
|
379
|
+
return { channel: "qqbot", error: "Voice not supported in channel" };
|
|
381
380
|
}
|
|
382
381
|
}
|
|
383
382
|
catch (err) {
|
|
@@ -417,8 +416,8 @@ export async function sendVideoMsg(ctx, videoPath) {
|
|
|
417
416
|
return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
|
|
418
417
|
}
|
|
419
418
|
else {
|
|
420
|
-
|
|
421
|
-
return { channel: "qqbot",
|
|
419
|
+
console.log(`${prefix} sendVideoMsg: video not supported in channel`);
|
|
420
|
+
return { channel: "qqbot", error: "Video not supported in channel" };
|
|
422
421
|
}
|
|
423
422
|
}
|
|
424
423
|
// 本地文件
|
|
@@ -441,7 +440,7 @@ export async function sendVideoMsg(ctx, videoPath) {
|
|
|
441
440
|
/** 从本地文件发送视频(sendVideoMsg 的内部辅助) */
|
|
442
441
|
async function sendVideoFromLocal(ctx, mediaPath, prefix) {
|
|
443
442
|
if (!(await fileExistsAsync(mediaPath))) {
|
|
444
|
-
return { channel: "qqbot", error: "
|
|
443
|
+
return { channel: "qqbot", error: "Video not found" };
|
|
445
444
|
}
|
|
446
445
|
const sizeCheck = checkFileSize(mediaPath);
|
|
447
446
|
if (!sizeCheck.ok) {
|
|
@@ -461,8 +460,8 @@ async function sendVideoFromLocal(ctx, mediaPath, prefix) {
|
|
|
461
460
|
return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
|
|
462
461
|
}
|
|
463
462
|
else {
|
|
464
|
-
|
|
465
|
-
return { channel: "qqbot",
|
|
463
|
+
console.log(`${prefix} sendVideoMsg: video not supported in channel`);
|
|
464
|
+
return { channel: "qqbot", error: "Video not supported in channel" };
|
|
466
465
|
}
|
|
467
466
|
}
|
|
468
467
|
catch (err) {
|
|
@@ -503,8 +502,8 @@ export async function sendDocument(ctx, filePath) {
|
|
|
503
502
|
return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
|
|
504
503
|
}
|
|
505
504
|
else {
|
|
506
|
-
|
|
507
|
-
return { channel: "qqbot",
|
|
505
|
+
console.log(`${prefix} sendDocument: file not supported in channel`);
|
|
506
|
+
return { channel: "qqbot", error: "File not supported in channel" };
|
|
508
507
|
}
|
|
509
508
|
}
|
|
510
509
|
// 本地文件
|
|
@@ -528,7 +527,7 @@ export async function sendDocument(ctx, filePath) {
|
|
|
528
527
|
async function sendDocumentFromLocal(ctx, mediaPath, prefix) {
|
|
529
528
|
const fileName = sanitizeFileName(path.basename(mediaPath));
|
|
530
529
|
if (!(await fileExistsAsync(mediaPath))) {
|
|
531
|
-
return { channel: "qqbot", error: "
|
|
530
|
+
return { channel: "qqbot", error: "File not found" };
|
|
532
531
|
}
|
|
533
532
|
const sizeCheck = checkFileSize(mediaPath);
|
|
534
533
|
if (!sizeCheck.ok) {
|
|
@@ -551,8 +550,8 @@ async function sendDocumentFromLocal(ctx, mediaPath, prefix) {
|
|
|
551
550
|
return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
|
|
552
551
|
}
|
|
553
552
|
else {
|
|
554
|
-
|
|
555
|
-
return { channel: "qqbot",
|
|
553
|
+
console.log(`${prefix} sendDocument: file not supported in channel`);
|
|
554
|
+
return { channel: "qqbot", error: "File not supported in channel" };
|
|
556
555
|
}
|
|
557
556
|
}
|
|
558
557
|
catch (err) {
|
|
@@ -18,7 +18,6 @@ import { getQQBotDataDir } from "./utils/platform.js";
|
|
|
18
18
|
// ============ 配置 ============
|
|
19
19
|
const STORAGE_DIR = getQQBotDataDir("data");
|
|
20
20
|
const REF_INDEX_FILE = path.join(STORAGE_DIR, "ref-index.jsonl");
|
|
21
|
-
const MAX_CONTENT_LENGTH = 500; // 存储的消息内容最大字符数
|
|
22
21
|
const MAX_ENTRIES = 50000; // 内存中最大缓存条目数
|
|
23
22
|
const TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 天
|
|
24
23
|
const COMPACT_THRESHOLD_RATIO = 2; // 文件行数超过有效条目 N 倍时 compact
|
|
@@ -164,7 +163,7 @@ export function setRefIndex(refIdx, entry) {
|
|
|
164
163
|
evictIfNeeded();
|
|
165
164
|
const now = Date.now();
|
|
166
165
|
store.set(refIdx, {
|
|
167
|
-
content: entry.content
|
|
166
|
+
content: entry.content,
|
|
168
167
|
senderId: entry.senderId,
|
|
169
168
|
senderName: entry.senderName,
|
|
170
169
|
timestamp: entry.timestamp,
|
|
@@ -176,7 +175,7 @@ export function setRefIndex(refIdx, entry) {
|
|
|
176
175
|
appendLine({
|
|
177
176
|
k: refIdx,
|
|
178
177
|
v: {
|
|
179
|
-
content: entry.content
|
|
178
|
+
content: entry.content,
|
|
180
179
|
senderId: entry.senderId,
|
|
181
180
|
senderName: entry.senderName,
|
|
182
181
|
timestamp: entry.timestamp,
|
|
@@ -12,16 +12,9 @@ export interface UpdateInfo {
|
|
|
12
12
|
checkedAt: number;
|
|
13
13
|
error?: string;
|
|
14
14
|
}
|
|
15
|
-
type UpdateFoundCallback = (info: UpdateInfo) => void;
|
|
16
|
-
/**
|
|
17
|
-
* 注册新版本发现回调(仅在首次检测到某个新版本时触发一次)
|
|
18
|
-
*/
|
|
19
|
-
export declare function onUpdateFound(cb: UpdateFoundCallback): void;
|
|
20
15
|
export declare function triggerUpdateCheck(log?: {
|
|
21
16
|
info: (msg: string) => void;
|
|
22
17
|
error: (msg: string) => void;
|
|
23
18
|
debug?: (msg: string) => void;
|
|
24
19
|
}): void;
|
|
25
20
|
export declare function getUpdateInfo(): UpdateInfo;
|
|
26
|
-
export declare function formatUpdateNotice(info: UpdateInfo): string;
|
|
27
|
-
export {};
|