@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 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.1`
13
+ ### 🚀 Current Version: `v1.6.2`
14
14
 
15
15
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
16
16
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](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.1 / 🌟 GitHub repo
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.1`
12
+ ### 🚀 当前版本: `v1.6.2`
13
13
 
14
14
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
15
15
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](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.1 / 🌟官方 GitHub 仓库
161
+ > **QQBot**:🦞框架版本:OpenClaw 2026.3.13 (61d171a) / 🤖QQBot 插件版本:v1.6.2 / 🌟官方 GitHub 仓库
162
162
 
163
163
  一目了然查看框架版本、插件版本,并可直接跳转官方仓库。
164
164
 
@@ -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>;
@@ -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
- if (text.length <= limit)
13
- return [text];
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: false,
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: chunkText,
190
+ chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
209
191
  chunkerMode: "markdown",
210
- textChunkLimit: 20000,
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 ? "..." : ""}`);
@@ -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, onUpdateFound, formatUpdateNotice } from "./update-checker.js";
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
- // 后台版本检查(detached 子进程,零阻塞)
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: (meta.text ?? "").slice(0, 500),
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
- try {
1510
- await sendWithTokenRetry(async (token) => {
1511
- const ref = consumeQuoteRef();
1512
- if (event.type === "c2c") {
1513
- return await sendC2CMessage(token, event.senderId, item.content, event.messageId, ref);
1514
- }
1515
- else if (event.type === "group" && event.groupOpenid) {
1516
- return await sendGroupMessage(token, event.groupOpenid, item.content, event.messageId);
1517
- }
1518
- else if (event.channelId) {
1519
- return await sendChannelMessage(token, event.channelId, item.content, event.messageId);
1520
- }
1521
- });
1522
- log?.info(`[qqbot:${account.accountId}] Sent text: ${item.content.slice(0, 50)}...`);
1523
- }
1524
- catch (err) {
1525
- log?.error(`[qqbot:${account.accountId}] Failed to send text: ${err}`);
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
- await sendErrorMessage(MSG.IMAGE_FORMAT_UNSUPPORTED(ext));
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
- await sendErrorMessage(MSG.VOICE_MISSING_TEXT);
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
- await sendChannelMessage(token, event.channelId, `${MSG.VOICE_CHANNEL_UNSUPPORTED}\n${ttsText}`, event.messageId);
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
- await sendErrorMessage(MSG.VIDEO_MISSING_PATH);
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
- await sendChannelMessage(token, event.channelId, MSG.VIDEO_CHANNEL_UNSUPPORTED, event.messageId);
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
- await sendChannelMessage(token, event.channelId, MSG.VIDEO_CHANNEL_UNSUPPORTED, event.messageId);
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
- await sendErrorMessage(MSG.FILE_MISSING_PATH);
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
- await sendChannelMessage(token, event.channelId, MSG.FILE_CHANNEL_UNSUPPORTED, event.messageId);
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
- await sendChannelMessage(token, event.channelId, MSG.FILE_CHANNEL_UNSUPPORTED, event.messageId);
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
- try {
2068
- await sendWithTokenRetry(async (token) => {
2069
- const ref = consumeQuoteRef();
2070
- if (event.type === "c2c") {
2071
- return await sendC2CMessage(token, event.senderId, textWithoutImages, event.messageId, ref);
2072
- }
2073
- else if (event.type === "group" && event.groupOpenid) {
2074
- return await sendGroupMessage(token, event.groupOpenid, textWithoutImages, event.messageId);
2075
- }
2076
- else if (event.channelId) {
2077
- return await sendChannelMessage(token, event.channelId, textWithoutImages, event.messageId);
2078
- }
2079
- });
2080
- log?.info(`[qqbot:${account.accountId}] Sent markdown message with ${httpImageUrls.length} HTTP images (${event.type})`);
2081
- }
2082
- catch (err) {
2083
- log?.error(`[qqbot:${account.accountId}] Failed to send markdown message: ${err}`);
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
- await sendWithTokenRetry(async (token) => {
2126
- const ref = consumeQuoteRef();
2127
- if (event.type === "c2c") {
2128
- return await sendC2CMessage(token, event.senderId, textWithoutImages, event.messageId, ref);
2129
- }
2130
- else if (event.type === "group" && event.groupOpenid) {
2131
- return await sendGroupMessage(token, event.groupOpenid, textWithoutImages, event.messageId);
2132
- }
2133
- else if (event.channelId) {
2134
- return await sendChannelMessage(token, event.channelId, textWithoutImages, event.messageId);
2135
- }
2136
- });
2137
- log?.info(`[qqbot:${account.accountId}] Sent text reply (${event.type})`);
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: false,
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;
@@ -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: MSG.IMAGE_FORMAT_UNSUPPORTED(ext) };
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
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VOICE_CHANNEL_UNSUPPORTED, ctx.replyToId);
322
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
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
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VOICE_CHANNEL_UNSUPPORTED, ctx.replyToId);
380
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
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
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VIDEO_CHANNEL_UNSUPPORTED, ctx.replyToId);
421
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
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
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VIDEO_CHANNEL_UNSUPPORTED, ctx.replyToId);
465
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
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
- const r = await sendChannelMessage(token, ctx.targetId, MSG.FILE_CHANNEL_UNSUPPORTED, ctx.replyToId);
507
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
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
- const r = await sendChannelMessage(token, ctx.targetId, MSG.FILE_CHANNEL_UNSUPPORTED, ctx.replyToId);
555
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
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) {
@@ -13,7 +13,7 @@
13
13
  * - t = 写入时间(用于 TTL 淘汰和 compact)
14
14
  */
15
15
  export interface RefIndexEntry {
16
- /** 消息文本内容摘要 */
16
+ /** 消息文本内容(完整保存) */
17
17
  content: string;
18
18
  /** 发送者 ID */
19
19
  senderId: string;
@@ -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.slice(0, MAX_CONTENT_LENGTH),
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.slice(0, MAX_CONTENT_LENGTH),
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 {};