@tencent-connect/openclaw-qqbot 1.6.2-alpha.1 → 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
 
@@ -15,7 +15,6 @@ 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, formatMediaErrorMessage } from "./user-messages.js";
19
18
  import { sendPhoto, sendVoice, sendVideoMsg, sendDocument, sendMedia as sendMediaAuto } from "./outbound.js";
20
19
  import { chunkText, TEXT_CHUNK_LIMIT } from "./channel.js";
21
20
  function resolveSTTConfig(cfg) {
@@ -347,7 +346,7 @@ export async function startGateway(ctx) {
347
346
  attachments.push(attachment);
348
347
  }
349
348
  setRefIndex(refIdx, {
350
- content: (meta.text ?? "").slice(0, 500),
349
+ content: meta.text ?? "",
351
350
  senderId: account.accountId,
352
351
  senderName: account.accountId,
353
352
  timestamp: Date.now(),
@@ -1507,7 +1506,6 @@ export async function startGateway(ctx) {
1507
1506
  const result = await sendPhoto(mediaTarget, item.content);
1508
1507
  if (result.error) {
1509
1508
  log?.error(`[qqbot:${account.accountId}] sendPhoto error: ${result.error}`);
1510
- await sendErrorMessage(formatMediaErrorMessage("图片", new Error(result.error)));
1511
1509
  }
1512
1510
  }
1513
1511
  else if (item.type === "voice") {
@@ -1522,26 +1520,22 @@ export async function startGateway(ctx) {
1522
1520
  ]);
1523
1521
  if (result.error) {
1524
1522
  log?.error(`[qqbot:${account.accountId}] sendVoice error: ${result.error}`);
1525
- await sendErrorMessage(formatMediaErrorMessage("语音", new Error(result.error)));
1526
1523
  }
1527
1524
  }
1528
1525
  catch (err) {
1529
1526
  log?.error(`[qqbot:${account.accountId}] sendVoice unexpected error: ${err}`);
1530
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1531
1527
  }
1532
1528
  }
1533
1529
  else if (item.type === "video") {
1534
1530
  const result = await sendVideoMsg(mediaTarget, item.content);
1535
1531
  if (result.error) {
1536
1532
  log?.error(`[qqbot:${account.accountId}] sendVideoMsg error: ${result.error}`);
1537
- await sendErrorMessage(formatMediaErrorMessage("视频", new Error(result.error)));
1538
1533
  }
1539
1534
  }
1540
1535
  else if (item.type === "file") {
1541
1536
  const result = await sendDocument(mediaTarget, item.content);
1542
1537
  if (result.error) {
1543
1538
  log?.error(`[qqbot:${account.accountId}] sendDocument error: ${result.error}`);
1544
- await sendErrorMessage(formatMediaErrorMessage("文件", new Error(result.error)));
1545
1539
  }
1546
1540
  }
1547
1541
  else if (item.type === "media") {
@@ -1556,7 +1550,6 @@ export async function startGateway(ctx) {
1556
1550
  });
1557
1551
  if (result.error) {
1558
1552
  log?.error(`[qqbot:${account.accountId}] sendMedia(auto) error: ${result.error}`);
1559
- await sendErrorMessage(formatMediaErrorMessage("媒体", new Error(result.error)));
1560
1553
  }
1561
1554
  }
1562
1555
  }
@@ -1573,9 +1566,7 @@ export async function startGateway(ctx) {
1573
1566
  const payloadResult = parseQQBotPayload(replyText);
1574
1567
  if (payloadResult.isPayload) {
1575
1568
  if (payloadResult.error) {
1576
- // 载荷解析失败,发送错误提示
1577
1569
  log?.error(`[qqbot:${account.accountId}] Payload parse error: ${payloadResult.error}`);
1578
- await sendErrorMessage(MSG.PAYLOAD_PARSE_ERROR);
1579
1570
  return;
1580
1571
  }
1581
1572
  if (payloadResult.payload) {
@@ -1625,12 +1616,12 @@ export async function startGateway(ctx) {
1625
1616
  if (parsedPayload.source === "file") {
1626
1617
  try {
1627
1618
  if (!(await fileExistsAsync(imageUrl))) {
1628
- await sendErrorMessage(MSG.IMAGE_NOT_FOUND);
1619
+ log?.error(`[qqbot:${account.accountId}] Image not found: ${imageUrl}`);
1629
1620
  return;
1630
1621
  }
1631
1622
  const imgSzCheck = checkFileSize(imageUrl);
1632
1623
  if (!imgSzCheck.ok) {
1633
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1624
+ log?.error(`[qqbot:${account.accountId}] Image size check failed: ${imgSzCheck.error}`);
1634
1625
  return;
1635
1626
  }
1636
1627
  const fileBuffer = await readFileAsync(imageUrl);
@@ -1646,7 +1637,7 @@ export async function startGateway(ctx) {
1646
1637
  };
1647
1638
  const mimeType = mimeTypes[ext];
1648
1639
  if (!mimeType) {
1649
- await sendErrorMessage(MSG.IMAGE_FORMAT_UNSUPPORTED(ext));
1640
+ log?.error(`[qqbot:${account.accountId}] Unsupported image format: ${ext}`);
1650
1641
  return;
1651
1642
  }
1652
1643
  imageUrl = `data:${mimeType};base64,${base64Data}`;
@@ -1654,7 +1645,6 @@ export async function startGateway(ctx) {
1654
1645
  }
1655
1646
  catch (readErr) {
1656
1647
  log?.error(`[qqbot:${account.accountId}] Failed to read local image: ${readErr}`);
1657
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1658
1648
  return;
1659
1649
  }
1660
1650
  }
@@ -1690,7 +1680,6 @@ export async function startGateway(ctx) {
1690
1680
  }
1691
1681
  catch (err) {
1692
1682
  log?.error(`[qqbot:${account.accountId}] Failed to send image: ${err}`);
1693
- await sendErrorMessage(formatMediaErrorMessage("图片", err));
1694
1683
  }
1695
1684
  }
1696
1685
  else if (parsedPayload.mediaType === "audio") {
@@ -1698,13 +1687,12 @@ export async function startGateway(ctx) {
1698
1687
  try {
1699
1688
  const ttsText = parsedPayload.caption || parsedPayload.path;
1700
1689
  if (!ttsText?.trim()) {
1701
- await sendErrorMessage(MSG.VOICE_MISSING_TEXT);
1690
+ log?.error(`[qqbot:${account.accountId}] Voice missing text`);
1702
1691
  }
1703
1692
  else {
1704
1693
  const ttsCfg = resolveTTSConfig(cfg);
1705
1694
  if (!ttsCfg) {
1706
1695
  log?.error(`[qqbot:${account.accountId}] TTS not configured (channels.qqbot.tts in openclaw.json)`);
1707
- await sendErrorMessage(MSG.VOICE_NOT_AVAILABLE);
1708
1696
  }
1709
1697
  else {
1710
1698
  log?.info(`[qqbot:${account.accountId}] TTS: "${ttsText.slice(0, 50)}..." via ${ttsCfg.model}`);
@@ -1719,7 +1707,8 @@ export async function startGateway(ctx) {
1719
1707
  await sendGroupVoiceMessage(token, event.groupOpenid, silkBase64, event.messageId);
1720
1708
  }
1721
1709
  else if (event.channelId) {
1722
- 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);
1723
1712
  }
1724
1713
  });
1725
1714
  log?.info(`[qqbot:${account.accountId}] Voice message sent`);
@@ -1728,7 +1717,6 @@ export async function startGateway(ctx) {
1728
1717
  }
1729
1718
  catch (err) {
1730
1719
  log?.error(`[qqbot:${account.accountId}] TTS/voice send failed: ${err}`);
1731
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1732
1720
  }
1733
1721
  }
1734
1722
  else if (parsedPayload.mediaType === "video") {
@@ -1736,7 +1724,7 @@ export async function startGateway(ctx) {
1736
1724
  try {
1737
1725
  const videoPath = normalizePath(parsedPayload.path ?? "");
1738
1726
  if (!videoPath?.trim()) {
1739
- await sendErrorMessage(MSG.VIDEO_MISSING_PATH);
1727
+ log?.error(`[qqbot:${account.accountId}] Video missing path`);
1740
1728
  }
1741
1729
  else {
1742
1730
  const isHttpUrl = videoPath.startsWith("http://") || videoPath.startsWith("https://");
@@ -1751,7 +1739,7 @@ export async function startGateway(ctx) {
1751
1739
  await sendGroupVideoMessage(token, event.groupOpenid, videoPath, undefined, event.messageId);
1752
1740
  }
1753
1741
  else if (event.channelId) {
1754
- await sendChannelMessage(token, event.channelId, MSG.VIDEO_CHANNEL_UNSUPPORTED, event.messageId);
1742
+ log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
1755
1743
  }
1756
1744
  }
1757
1745
  else {
@@ -1773,7 +1761,7 @@ export async function startGateway(ctx) {
1773
1761
  await sendGroupVideoMessage(token, event.groupOpenid, undefined, videoBase64, event.messageId);
1774
1762
  }
1775
1763
  else if (event.channelId) {
1776
- await sendChannelMessage(token, event.channelId, MSG.VIDEO_CHANNEL_UNSUPPORTED, event.messageId);
1764
+ log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
1777
1765
  }
1778
1766
  }
1779
1767
  });
@@ -1796,7 +1784,6 @@ export async function startGateway(ctx) {
1796
1784
  }
1797
1785
  catch (err) {
1798
1786
  log?.error(`[qqbot:${account.accountId}] Video send failed: ${err}`);
1799
- await sendErrorMessage(formatMediaErrorMessage("视频", err));
1800
1787
  }
1801
1788
  }
1802
1789
  else if (parsedPayload.mediaType === "file") {
@@ -1804,7 +1791,7 @@ export async function startGateway(ctx) {
1804
1791
  try {
1805
1792
  const filePath = normalizePath(parsedPayload.path ?? "");
1806
1793
  if (!filePath?.trim()) {
1807
- await sendErrorMessage(MSG.FILE_MISSING_PATH);
1794
+ log?.error(`[qqbot:${account.accountId}] File missing path`);
1808
1795
  }
1809
1796
  else {
1810
1797
  const isHttpUrl = filePath.startsWith("http://") || filePath.startsWith("https://");
@@ -1819,7 +1806,7 @@ export async function startGateway(ctx) {
1819
1806
  await sendGroupFileMessage(token, event.groupOpenid, undefined, filePath, event.messageId, fileName);
1820
1807
  }
1821
1808
  else if (event.channelId) {
1822
- await sendChannelMessage(token, event.channelId, MSG.FILE_CHANNEL_UNSUPPORTED, event.messageId);
1809
+ log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
1823
1810
  }
1824
1811
  }
1825
1812
  else {
@@ -1839,7 +1826,7 @@ export async function startGateway(ctx) {
1839
1826
  await sendGroupFileMessage(token, event.groupOpenid, fileBase64, undefined, event.messageId, fileName);
1840
1827
  }
1841
1828
  else if (event.channelId) {
1842
- await sendChannelMessage(token, event.channelId, MSG.FILE_CHANNEL_UNSUPPORTED, event.messageId);
1829
+ log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
1843
1830
  }
1844
1831
  }
1845
1832
  });
@@ -1848,12 +1835,10 @@ export async function startGateway(ctx) {
1848
1835
  }
1849
1836
  catch (err) {
1850
1837
  log?.error(`[qqbot:${account.accountId}] File send failed: ${err}`);
1851
- await sendErrorMessage(formatMediaErrorMessage("文件", err));
1852
1838
  }
1853
1839
  }
1854
1840
  else {
1855
1841
  log?.error(`[qqbot:${account.accountId}] Unknown media type: ${parsedPayload.mediaType}`);
1856
- await sendErrorMessage(MSG.UNSUPPORTED_MEDIA_TYPE);
1857
1842
  }
1858
1843
  // 记录活动并返回
1859
1844
  pluginRuntime.channel.activity.record({
@@ -1866,7 +1851,6 @@ export async function startGateway(ctx) {
1866
1851
  else {
1867
1852
  // 未知的载荷类型
1868
1853
  log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${parsedPayload.type}`);
1869
- await sendErrorMessage(MSG.UNSUPPORTED_PAYLOAD_TYPE);
1870
1854
  return;
1871
1855
  }
1872
1856
  }
@@ -2207,10 +2191,10 @@ export async function startGateway(ctx) {
2207
2191
  // 发送错误提示给用户,显示完整错误信息
2208
2192
  const errMsg = String(err);
2209
2193
  if (errMsg.includes("401") || errMsg.includes("key") || errMsg.includes("auth")) {
2210
- await sendErrorMessage(MSG.AI_AUTH_ERROR);
2194
+ log?.error(`[qqbot:${account.accountId}] AI auth error: ${errMsg}`);
2211
2195
  }
2212
2196
  else {
2213
- await sendErrorMessage(MSG.AI_PROCESS_ERROR);
2197
+ log?.error(`[qqbot:${account.accountId}] AI process error: ${errMsg}`);
2214
2198
  }
2215
2199
  },
2216
2200
  },
@@ -2228,7 +2212,6 @@ export async function startGateway(ctx) {
2228
2212
  }
2229
2213
  if (!hasResponse) {
2230
2214
  log?.error(`[qqbot:${account.accountId}] No response within timeout`);
2231
- await sendErrorMessage(MSG.TIMEOUT_HINT);
2232
2215
  }
2233
2216
  }
2234
2217
  finally {
@@ -2247,7 +2230,6 @@ export async function startGateway(ctx) {
2247
2230
  }
2248
2231
  catch (err) {
2249
2232
  log?.error(`[qqbot:${account.accountId}] Message processing failed: ${err}`);
2250
- await sendErrorMessage(MSG.GENERIC_ERROR);
2251
2233
  }
2252
2234
  };
2253
2235
  ws.on("open", () => {
@@ -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: MSG.IMAGE_NOT_FOUND };
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: MSG.VOICE_GENERATE_FAILED };
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: MSG.VIDEO_NOT_FOUND };
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: MSG.FILE_NOT_FOUND };
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,
@@ -1,43 +1,8 @@
1
+ export {};
1
2
  /**
2
- * 用户面向的提示文案集中管理
3
+ * 用户面向的提示文案 — 已清空
3
4
  *
4
- * 设计原则(参考 Telegram / Discord / Slack / 飞书):
5
- * 1. 禁止暴露服务器路径、原始 Error、配置文件结构
6
- * 2. 错误信息分级:用户只看到通用友好文案,技术细节走日志
7
- * 3. 统一风格:去掉 [QQBot] 前缀和 [方括号] 格式
8
- * 4. 所有面向用户的文案集中在此文件,便于维护和国际化
5
+ * 设计原则(对齐飞书插件):
6
+ * QQBot 插件层不生成额外的用户提示信息。
7
+ * 所有运行时错误仅写日志,不面向用户展示。
9
8
  */
10
- export declare const MSG: {
11
- readonly GENERIC_ERROR: "抱歉,处理消息时遇到了问题,请稍后再试~";
12
- readonly AI_AUTH_ERROR: "抱歉,AI 服务暂时不可用,请联系管理员检查配置~";
13
- readonly AI_PROCESS_ERROR: "抱歉,AI 处理遇到了问题,请稍后再试~";
14
- readonly TIMEOUT_HINT: "已收到消息,正在处理中…";
15
- readonly IMAGE_NOT_FOUND: "抱歉,图片不存在或已失效,无法发送~";
16
- readonly IMAGE_FORMAT_UNSUPPORTED: (ext: string) => string;
17
- readonly IMAGE_SEND_FAILED: "抱歉,图片发送失败了,请稍后再试~";
18
- readonly IMAGE_UPLOADING: (size: string) => string;
19
- readonly VOICE_GENERATE_FAILED: "抱歉,语音生成失败,请稍后重试~";
20
- readonly VOICE_CONVERT_FAILED: "抱歉,语音格式转换失败,请稍后重试~";
21
- readonly VOICE_SEND_FAILED: "抱歉,语音发送失败了,请稍后再试~";
22
- readonly VOICE_NOT_AVAILABLE: "抱歉,语音功能暂未开启~";
23
- readonly VOICE_MISSING_TEXT: "抱歉,语音消息缺少内容~";
24
- readonly VOICE_CHANNEL_UNSUPPORTED: "抱歉,语音消息暂不支持在频道中发送~";
25
- readonly VIDEO_NOT_FOUND: "抱歉,视频文件不存在或已失效,无法发送~";
26
- readonly VIDEO_SEND_FAILED: "抱歉,视频发送失败了,请稍后再试~";
27
- readonly VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~";
28
- readonly VIDEO_CHANNEL_UNSUPPORTED: "抱歉,视频消息暂不支持在频道中发送~";
29
- readonly VIDEO_UPLOADING: (size: string) => string;
30
- readonly FILE_NOT_FOUND: "抱歉,文件不存在或已失效,无法发送~";
31
- readonly FILE_SEND_FAILED: "抱歉,文件发送失败了,请稍后再试~";
32
- readonly FILE_MISSING_PATH: "抱歉,文件消息缺少内容~";
33
- readonly FILE_CHANNEL_UNSUPPORTED: "抱歉,文件消息暂不支持在频道中发送~";
34
- readonly FILE_UPLOADING: (name: string, size: string) => string;
35
- readonly PAYLOAD_PARSE_ERROR: "抱歉,消息格式异常,无法处理~";
36
- readonly UNSUPPORTED_MEDIA_TYPE: "抱歉,暂不支持该媒体类型~";
37
- readonly UNSUPPORTED_PAYLOAD_TYPE: "抱歉,暂不支持该消息类型~";
38
- };
39
- /**
40
- * 将媒体上传/发送错误转为对用户友好的提示文案
41
- * 技术细节不暴露给用户,仅记录到日志
42
- */
43
- export declare function formatMediaErrorMessage(mediaType: string, err: unknown): string;
@@ -1,65 +1,8 @@
1
+ export {};
1
2
  /**
2
- * 用户面向的提示文案集中管理
3
+ * 用户面向的提示文案 — 已清空
3
4
  *
4
- * 设计原则(参考 Telegram / Discord / Slack / 飞书):
5
- * 1. 禁止暴露服务器路径、原始 Error、配置文件结构
6
- * 2. 错误信息分级:用户只看到通用友好文案,技术细节走日志
7
- * 3. 统一风格:去掉 [QQBot] 前缀和 [方括号] 格式
8
- * 4. 所有面向用户的文案集中在此文件,便于维护和国际化
5
+ * 设计原则(对齐飞书插件):
6
+ * QQBot 插件层不生成额外的用户提示信息。
7
+ * 所有运行时错误仅写日志,不面向用户展示。
9
8
  */
10
- // ============ 媒体发送错误 ============
11
- export const MSG = {
12
- // 通用错误
13
- GENERIC_ERROR: "抱歉,处理消息时遇到了问题,请稍后再试~",
14
- AI_AUTH_ERROR: "抱歉,AI 服务暂时不可用,请联系管理员检查配置~",
15
- AI_PROCESS_ERROR: "抱歉,AI 处理遇到了问题,请稍后再试~",
16
- TIMEOUT_HINT: "已收到消息,正在处理中…",
17
- // 图片
18
- IMAGE_NOT_FOUND: "抱歉,图片不存在或已失效,无法发送~",
19
- IMAGE_FORMAT_UNSUPPORTED: (ext) => `抱歉,暂不支持 ${ext} 格式的图片~`,
20
- IMAGE_SEND_FAILED: "抱歉,图片发送失败了,请稍后再试~",
21
- IMAGE_UPLOADING: (size) => `正在上传图片 (${size})...`,
22
- // 语音
23
- VOICE_GENERATE_FAILED: "抱歉,语音生成失败,请稍后重试~",
24
- VOICE_CONVERT_FAILED: "抱歉,语音格式转换失败,请稍后重试~",
25
- VOICE_SEND_FAILED: "抱歉,语音发送失败了,请稍后再试~",
26
- VOICE_NOT_AVAILABLE: "抱歉,语音功能暂未开启~",
27
- VOICE_MISSING_TEXT: "抱歉,语音消息缺少内容~",
28
- VOICE_CHANNEL_UNSUPPORTED: "抱歉,语音消息暂不支持在频道中发送~",
29
- // 视频
30
- VIDEO_NOT_FOUND: "抱歉,视频文件不存在或已失效,无法发送~",
31
- VIDEO_SEND_FAILED: "抱歉,视频发送失败了,请稍后再试~",
32
- VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~",
33
- VIDEO_CHANNEL_UNSUPPORTED: "抱歉,视频消息暂不支持在频道中发送~",
34
- VIDEO_UPLOADING: (size) => `正在上传视频 (${size})...`,
35
- // 文件
36
- FILE_NOT_FOUND: "抱歉,文件不存在或已失效,无法发送~",
37
- FILE_SEND_FAILED: "抱歉,文件发送失败了,请稍后再试~",
38
- FILE_MISSING_PATH: "抱歉,文件消息缺少内容~",
39
- FILE_CHANNEL_UNSUPPORTED: "抱歉,文件消息暂不支持在频道中发送~",
40
- FILE_UPLOADING: (name, size) => `正在上传文件 ${name} (${size})...`,
41
- // 载荷解析
42
- PAYLOAD_PARSE_ERROR: "抱歉,消息格式异常,无法处理~",
43
- UNSUPPORTED_MEDIA_TYPE: "抱歉,暂不支持该媒体类型~",
44
- UNSUPPORTED_PAYLOAD_TYPE: "抱歉,暂不支持该消息类型~",
45
- };
46
- /**
47
- * 将媒体上传/发送错误转为对用户友好的提示文案
48
- * 技术细节不暴露给用户,仅记录到日志
49
- */
50
- export function formatMediaErrorMessage(mediaType, err) {
51
- const msg = err instanceof Error ? err.message : String(err);
52
- if (msg.includes("上传超时") || msg.includes("timeout") || msg.includes("Timeout")) {
53
- return `抱歉,${mediaType}资源加载超时,可能是网络原因或文件太大,请稍后再试~`;
54
- }
55
- if (msg.includes("文件不存在") || msg.includes("not found") || msg.includes("Not Found")) {
56
- return `抱歉,${mediaType}文件不存在或已失效,无法发送~`;
57
- }
58
- if (msg.includes("文件大小") || msg.includes("too large") || msg.includes("exceed")) {
59
- return `抱歉,${mediaType}文件太大了,超出了发送限制~`;
60
- }
61
- if (msg.includes("Network error") || msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND")) {
62
- return `抱歉,网络连接异常,${mediaType}发送失败,请稍后再试~`;
63
- }
64
- return `抱歉,${mediaType}发送失败了,请稍后再试~`;
65
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.2-alpha.1",
3
+ "version": "1.6.2-alpha.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/gateway.ts CHANGED
@@ -16,7 +16,7 @@ import { convertSilkToWav, isVoiceAttachment, formatDuration, resolveTTSConfig,
16
16
  import { normalizeMediaTags } from "./utils/media-tags.js";
17
17
  import { checkFileSize, readFileAsync, fileExistsAsync, isLargeFile, formatFileSize } from "./utils/file-utils.js";
18
18
  import { getQQBotDataDir, isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, runDiagnostics } from "./utils/platform.js";
19
- import { MSG, formatMediaErrorMessage } from "./user-messages.js";
19
+
20
20
  import { sendPhoto, sendVoice, sendVideoMsg, sendDocument, sendMedia as sendMediaAuto, type MediaTargetContext } from "./outbound.js";
21
21
  import { chunkText, TEXT_CHUNK_LIMIT } from "./channel.js";
22
22
 
@@ -444,7 +444,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
444
444
  attachments.push(attachment);
445
445
  }
446
446
  setRefIndex(refIdx, {
447
- content: (meta.text ?? "").slice(0, 500),
447
+ content: meta.text ?? "",
448
448
  senderId: account.accountId,
449
449
  senderName: account.accountId,
450
450
  timestamp: Date.now(),
@@ -1683,7 +1683,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1683
1683
  const result = await sendPhoto(mediaTarget, item.content);
1684
1684
  if (result.error) {
1685
1685
  log?.error(`[qqbot:${account.accountId}] sendPhoto error: ${result.error}`);
1686
- await sendErrorMessage(formatMediaErrorMessage("图片", new Error(result.error)));
1687
1686
  }
1688
1687
  } else if (item.type === "voice") {
1689
1688
  const uploadFormats = account.config?.audioFormatPolicy?.uploadDirectFormats ?? account.config?.voiceDirectUploadFormats;
@@ -1699,23 +1698,19 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1699
1698
  ]);
1700
1699
  if (result.error) {
1701
1700
  log?.error(`[qqbot:${account.accountId}] sendVoice error: ${result.error}`);
1702
- await sendErrorMessage(formatMediaErrorMessage("语音", new Error(result.error)));
1703
1701
  }
1704
1702
  } catch (err) {
1705
1703
  log?.error(`[qqbot:${account.accountId}] sendVoice unexpected error: ${err}`);
1706
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1707
1704
  }
1708
1705
  } else if (item.type === "video") {
1709
1706
  const result = await sendVideoMsg(mediaTarget, item.content);
1710
1707
  if (result.error) {
1711
1708
  log?.error(`[qqbot:${account.accountId}] sendVideoMsg error: ${result.error}`);
1712
- await sendErrorMessage(formatMediaErrorMessage("视频", new Error(result.error)));
1713
1709
  }
1714
1710
  } else if (item.type === "file") {
1715
1711
  const result = await sendDocument(mediaTarget, item.content);
1716
1712
  if (result.error) {
1717
1713
  log?.error(`[qqbot:${account.accountId}] sendDocument error: ${result.error}`);
1718
- await sendErrorMessage(formatMediaErrorMessage("文件", new Error(result.error)));
1719
1714
  }
1720
1715
  } else if (item.type === "media") {
1721
1716
  // qqmedia: 自动根据扩展名路由到 sendPhoto/sendVoice/sendVideoMsg/sendDocument
@@ -1729,7 +1724,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1729
1724
  });
1730
1725
  if (result.error) {
1731
1726
  log?.error(`[qqbot:${account.accountId}] sendMedia(auto) error: ${result.error}`);
1732
- await sendErrorMessage(formatMediaErrorMessage("媒体", new Error(result.error)));
1733
1727
  }
1734
1728
  }
1735
1729
  }
@@ -1749,9 +1743,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1749
1743
 
1750
1744
  if (payloadResult.isPayload) {
1751
1745
  if (payloadResult.error) {
1752
- // 载荷解析失败,发送错误提示
1753
1746
  log?.error(`[qqbot:${account.accountId}] Payload parse error: ${payloadResult.error}`);
1754
- await sendErrorMessage(MSG.PAYLOAD_PARSE_ERROR);
1755
1747
  return;
1756
1748
  }
1757
1749
 
@@ -1804,12 +1796,12 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1804
1796
  if (parsedPayload.source === "file") {
1805
1797
  try {
1806
1798
  if (!(await fileExistsAsync(imageUrl))) {
1807
- await sendErrorMessage(MSG.IMAGE_NOT_FOUND);
1799
+ log?.error(`[qqbot:${account.accountId}] Image not found: ${imageUrl}`);
1808
1800
  return;
1809
1801
  }
1810
1802
  const imgSzCheck = checkFileSize(imageUrl);
1811
1803
  if (!imgSzCheck.ok) {
1812
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1804
+ log?.error(`[qqbot:${account.accountId}] Image size check failed: ${imgSzCheck.error}`);
1813
1805
  return;
1814
1806
  }
1815
1807
  const fileBuffer = await readFileAsync(imageUrl);
@@ -1825,14 +1817,13 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1825
1817
  };
1826
1818
  const mimeType = mimeTypes[ext];
1827
1819
  if (!mimeType) {
1828
- await sendErrorMessage(MSG.IMAGE_FORMAT_UNSUPPORTED(ext));
1820
+ log?.error(`[qqbot:${account.accountId}] Unsupported image format: ${ext}`);
1829
1821
  return;
1830
1822
  }
1831
1823
  imageUrl = `data:${mimeType};base64,${base64Data}`;
1832
1824
  log?.info(`[qqbot:${account.accountId}] Converted local image to Base64 (size: ${formatFileSize(fileBuffer.length)})`);
1833
1825
  } catch (readErr) {
1834
1826
  log?.error(`[qqbot:${account.accountId}] Failed to read local image: ${readErr}`);
1835
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1836
1827
  return;
1837
1828
  }
1838
1829
  }
@@ -1865,19 +1856,17 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1865
1856
  }
1866
1857
  } catch (err) {
1867
1858
  log?.error(`[qqbot:${account.accountId}] Failed to send image: ${err}`);
1868
- await sendErrorMessage(formatMediaErrorMessage("图片", err));
1869
1859
  }
1870
1860
  } else if (parsedPayload.mediaType === "audio") {
1871
1861
  // TTS 语音发送:文字 → PCM → SILK → QQ 语音
1872
1862
  try {
1873
1863
  const ttsText = parsedPayload.caption || parsedPayload.path;
1874
1864
  if (!ttsText?.trim()) {
1875
- await sendErrorMessage(MSG.VOICE_MISSING_TEXT);
1865
+ log?.error(`[qqbot:${account.accountId}] Voice missing text`);
1876
1866
  } else {
1877
1867
  const ttsCfg = resolveTTSConfig(cfg as Record<string, unknown>);
1878
1868
  if (!ttsCfg) {
1879
1869
  log?.error(`[qqbot:${account.accountId}] TTS not configured (channels.qqbot.tts in openclaw.json)`);
1880
- await sendErrorMessage(MSG.VOICE_NOT_AVAILABLE);
1881
1870
  } else {
1882
1871
  log?.info(`[qqbot:${account.accountId}] TTS: "${ttsText.slice(0, 50)}..." via ${ttsCfg.model}`);
1883
1872
  const ttsDir = getQQBotDataDir("tts");
@@ -1890,7 +1879,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1890
1879
  } else if (event.type === "group" && event.groupOpenid) {
1891
1880
  await sendGroupVoiceMessage(token, event.groupOpenid, silkBase64, event.messageId);
1892
1881
  } else if (event.channelId) {
1893
- await sendChannelMessage(token, event.channelId, `${MSG.VOICE_CHANNEL_UNSUPPORTED}\n${ttsText}`, event.messageId);
1882
+ log?.error(`[qqbot:${account.accountId}] Voice not supported in channel, sending text fallback`);
1883
+ await sendChannelMessage(token, event.channelId, ttsText, event.messageId);
1894
1884
  }
1895
1885
  });
1896
1886
  log?.info(`[qqbot:${account.accountId}] Voice message sent`);
@@ -1898,14 +1888,13 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1898
1888
  }
1899
1889
  } catch (err) {
1900
1890
  log?.error(`[qqbot:${account.accountId}] TTS/voice send failed: ${err}`);
1901
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1902
1891
  }
1903
1892
  } else if (parsedPayload.mediaType === "video") {
1904
1893
  // 视频发送:支持公网 URL 和本地文件
1905
1894
  try {
1906
1895
  const videoPath = normalizePath(parsedPayload.path ?? "");
1907
1896
  if (!videoPath?.trim()) {
1908
- await sendErrorMessage(MSG.VIDEO_MISSING_PATH);
1897
+ log?.error(`[qqbot:${account.accountId}] Video missing path`);
1909
1898
  } else {
1910
1899
  const isHttpUrl = videoPath.startsWith("http://") || videoPath.startsWith("https://");
1911
1900
  log?.info(`[qqbot:${account.accountId}] Video send: "${videoPath.slice(0, 60)}..."`);
@@ -1918,7 +1907,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1918
1907
  } else if (event.type === "group" && event.groupOpenid) {
1919
1908
  await sendGroupVideoMessage(token, event.groupOpenid, videoPath, undefined, event.messageId);
1920
1909
  } else if (event.channelId) {
1921
- await sendChannelMessage(token, event.channelId, MSG.VIDEO_CHANNEL_UNSUPPORTED, event.messageId);
1910
+ log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
1922
1911
  }
1923
1912
  } else {
1924
1913
  // 本地文件:读取为 Base64
@@ -1938,7 +1927,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1938
1927
  } else if (event.type === "group" && event.groupOpenid) {
1939
1928
  await sendGroupVideoMessage(token, event.groupOpenid, undefined, videoBase64, event.messageId);
1940
1929
  } else if (event.channelId) {
1941
- await sendChannelMessage(token, event.channelId, MSG.VIDEO_CHANNEL_UNSUPPORTED, event.messageId);
1930
+ log?.error(`[qqbot:${account.accountId}] Video not supported in channel`);
1942
1931
  }
1943
1932
  }
1944
1933
  });
@@ -1959,14 +1948,13 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1959
1948
  }
1960
1949
  } catch (err) {
1961
1950
  log?.error(`[qqbot:${account.accountId}] Video send failed: ${err}`);
1962
- await sendErrorMessage(formatMediaErrorMessage("视频", err));
1963
1951
  }
1964
1952
  } else if (parsedPayload.mediaType === "file") {
1965
1953
  // 文件发送
1966
1954
  try {
1967
1955
  const filePath = normalizePath(parsedPayload.path ?? "");
1968
1956
  if (!filePath?.trim()) {
1969
- await sendErrorMessage(MSG.FILE_MISSING_PATH);
1957
+ log?.error(`[qqbot:${account.accountId}] File missing path`);
1970
1958
  } else {
1971
1959
  const isHttpUrl = filePath.startsWith("http://") || filePath.startsWith("https://");
1972
1960
  const fileName = sanitizeFileName(path.basename(filePath));
@@ -1979,7 +1967,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1979
1967
  } else if (event.type === "group" && event.groupOpenid) {
1980
1968
  await sendGroupFileMessage(token, event.groupOpenid, undefined, filePath, event.messageId, fileName);
1981
1969
  } else if (event.channelId) {
1982
- await sendChannelMessage(token, event.channelId, MSG.FILE_CHANNEL_UNSUPPORTED, event.messageId);
1970
+ log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
1983
1971
  }
1984
1972
  } else {
1985
1973
  if (!(await fileExistsAsync(filePath))) {
@@ -1996,7 +1984,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1996
1984
  } else if (event.type === "group" && event.groupOpenid) {
1997
1985
  await sendGroupFileMessage(token, event.groupOpenid, fileBase64, undefined, event.messageId, fileName);
1998
1986
  } else if (event.channelId) {
1999
- await sendChannelMessage(token, event.channelId, MSG.FILE_CHANNEL_UNSUPPORTED, event.messageId);
1987
+ log?.error(`[qqbot:${account.accountId}] File not supported in channel`);
2000
1988
  }
2001
1989
  }
2002
1990
  });
@@ -2004,11 +1992,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2004
1992
  }
2005
1993
  } catch (err) {
2006
1994
  log?.error(`[qqbot:${account.accountId}] File send failed: ${err}`);
2007
- await sendErrorMessage(formatMediaErrorMessage("文件", err));
2008
1995
  }
2009
1996
  } else {
2010
1997
  log?.error(`[qqbot:${account.accountId}] Unknown media type: ${(parsedPayload as MediaPayload).mediaType}`);
2011
- await sendErrorMessage(MSG.UNSUPPORTED_MEDIA_TYPE);
2012
1998
  }
2013
1999
 
2014
2000
  // 记录活动并返回
@@ -2021,7 +2007,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2021
2007
  } else {
2022
2008
  // 未知的载荷类型
2023
2009
  log?.error(`[qqbot:${account.accountId}] Unknown payload type: ${(parsedPayload as any).type}`);
2024
- await sendErrorMessage(MSG.UNSUPPORTED_PAYLOAD_TYPE);
2025
2010
  return;
2026
2011
  }
2027
2012
  }
@@ -2370,9 +2355,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2370
2355
  // 发送错误提示给用户,显示完整错误信息
2371
2356
  const errMsg = String(err);
2372
2357
  if (errMsg.includes("401") || errMsg.includes("key") || errMsg.includes("auth")) {
2373
- await sendErrorMessage(MSG.AI_AUTH_ERROR);
2358
+ log?.error(`[qqbot:${account.accountId}] AI auth error: ${errMsg}`);
2374
2359
  } else {
2375
- await sendErrorMessage(MSG.AI_PROCESS_ERROR);
2360
+ log?.error(`[qqbot:${account.accountId}] AI process error: ${errMsg}`);
2376
2361
  }
2377
2362
  },
2378
2363
  },
@@ -2390,7 +2375,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2390
2375
  }
2391
2376
  if (!hasResponse) {
2392
2377
  log?.error(`[qqbot:${account.accountId}] No response within timeout`);
2393
- await sendErrorMessage(MSG.TIMEOUT_HINT);
2394
2378
  }
2395
2379
  } finally {
2396
2380
  // 清理 tool-only 兜底定时器
@@ -2407,7 +2391,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2407
2391
  }
2408
2392
  } catch (err) {
2409
2393
  log?.error(`[qqbot:${account.accountId}] Message processing failed: ${err}`);
2410
- await sendErrorMessage(MSG.GENERIC_ERROR);
2411
2394
  }
2412
2395
  };
2413
2396
 
package/src/outbound.ts CHANGED
@@ -26,7 +26,6 @@ import { normalizeMediaTags } from "./utils/media-tags.js";
26
26
  import { checkFileSize, readFileAsync, fileExistsAsync, isLargeFile, formatFileSize } from "./utils/file-utils.js";
27
27
  import { isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, getQQBotDataDir } from "./utils/platform.js";
28
28
  import { downloadFile } from "./image-server.js";
29
- import { MSG } from "./user-messages.js";
30
29
 
31
30
  // ============ 消息回复限流器 ============
32
31
  // 同一 message_id 1小时内最多回复 4 次,超过 1 小时无法被动回复(需改为主动消息)
@@ -313,7 +312,7 @@ export async function sendPhoto(
313
312
 
314
313
  if (isLocal) {
315
314
  if (!(await fileExistsAsync(mediaPath))) {
316
- return { channel: "qqbot", error: MSG.IMAGE_NOT_FOUND };
315
+ return { channel: "qqbot", error: "Image not found" };
317
316
  }
318
317
  const sizeCheck = checkFileSize(mediaPath);
319
318
  if (!sizeCheck.ok) {
@@ -327,7 +326,7 @@ export async function sendPhoto(
327
326
  };
328
327
  const mimeType = mimeTypes[ext];
329
328
  if (!mimeType) {
330
- return { channel: "qqbot", error: MSG.IMAGE_FORMAT_UNSUPPORTED(ext) };
329
+ return { channel: "qqbot", error: `Unsupported image format: ${ext}` };
331
330
  }
332
331
  imageUrl = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
333
332
  console.log(`${prefix} sendPhoto: local → Base64 (${formatFileSize(fileBuffer.length)})`);
@@ -430,8 +429,8 @@ export async function sendVoice(
430
429
  const r = await sendGroupVoiceMessage(token, ctx.targetId, undefined, mediaPath, ctx.replyToId);
431
430
  return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
432
431
  } else {
433
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VOICE_CHANNEL_UNSUPPORTED, ctx.replyToId);
434
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
432
+ console.log(`${prefix} sendVoice: voice not supported in channel`);
433
+ return { channel: "qqbot", error: "Voice not supported in channel" };
435
434
  }
436
435
  } catch (err) {
437
436
  const msg = err instanceof Error ? err.message : String(err);
@@ -464,7 +463,7 @@ async function sendVoiceFromLocal(
464
463
  // 等待文件就绪(TTS 异步生成,文件可能还没写完)
465
464
  const fileSize = await waitForFile(mediaPath);
466
465
  if (fileSize === 0) {
467
- return { channel: "qqbot", error: MSG.VOICE_GENERATE_FAILED };
466
+ return { channel: "qqbot", error: "Voice generate failed" };
468
467
  }
469
468
 
470
469
  // 精细检测:是否需要转码
@@ -498,8 +497,8 @@ async function sendVoiceFromLocal(
498
497
  const r = await sendGroupVoiceMessage(token, ctx.targetId, uploadBase64, undefined, ctx.replyToId);
499
498
  return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
500
499
  } else {
501
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VOICE_CHANNEL_UNSUPPORTED, ctx.replyToId);
502
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
500
+ console.log(`${prefix} sendVoice: voice not supported in channel`);
501
+ return { channel: "qqbot", error: "Voice not supported in channel" };
503
502
  }
504
503
  } catch (err) {
505
504
  const msg = err instanceof Error ? err.message : String(err);
@@ -543,8 +542,8 @@ export async function sendVideoMsg(
543
542
  const r = await sendGroupVideoMessage(token, ctx.targetId, mediaPath, undefined, ctx.replyToId);
544
543
  return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
545
544
  } else {
546
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VIDEO_CHANNEL_UNSUPPORTED, ctx.replyToId);
547
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
545
+ console.log(`${prefix} sendVideoMsg: video not supported in channel`);
546
+ return { channel: "qqbot", error: "Video not supported in channel" };
548
547
  }
549
548
  }
550
549
 
@@ -570,7 +569,7 @@ export async function sendVideoMsg(
570
569
  /** 从本地文件发送视频(sendVideoMsg 的内部辅助) */
571
570
  async function sendVideoFromLocal(ctx: MediaTargetContext, mediaPath: string, prefix: string): Promise<OutboundResult> {
572
571
  if (!(await fileExistsAsync(mediaPath))) {
573
- return { channel: "qqbot", error: MSG.VIDEO_NOT_FOUND };
572
+ return { channel: "qqbot", error: "Video not found" };
574
573
  }
575
574
  const sizeCheck = checkFileSize(mediaPath);
576
575
  if (!sizeCheck.ok) {
@@ -590,8 +589,8 @@ async function sendVideoFromLocal(ctx: MediaTargetContext, mediaPath: string, pr
590
589
  const r = await sendGroupVideoMessage(token, ctx.targetId, undefined, videoBase64, ctx.replyToId);
591
590
  return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
592
591
  } else {
593
- const r = await sendChannelMessage(token, ctx.targetId, MSG.VIDEO_CHANNEL_UNSUPPORTED, ctx.replyToId);
594
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
592
+ console.log(`${prefix} sendVideoMsg: video not supported in channel`);
593
+ return { channel: "qqbot", error: "Video not supported in channel" };
595
594
  }
596
595
  } catch (err) {
597
596
  const msg = err instanceof Error ? err.message : String(err);
@@ -636,8 +635,8 @@ export async function sendDocument(
636
635
  const r = await sendGroupFileMessage(token, ctx.targetId, undefined, mediaPath, ctx.replyToId, fileName);
637
636
  return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
638
637
  } else {
639
- const r = await sendChannelMessage(token, ctx.targetId, MSG.FILE_CHANNEL_UNSUPPORTED, ctx.replyToId);
640
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
638
+ console.log(`${prefix} sendDocument: file not supported in channel`);
639
+ return { channel: "qqbot", error: "File not supported in channel" };
641
640
  }
642
641
  }
643
642
 
@@ -665,7 +664,7 @@ async function sendDocumentFromLocal(ctx: MediaTargetContext, mediaPath: string,
665
664
  const fileName = sanitizeFileName(path.basename(mediaPath));
666
665
 
667
666
  if (!(await fileExistsAsync(mediaPath))) {
668
- return { channel: "qqbot", error: MSG.FILE_NOT_FOUND };
667
+ return { channel: "qqbot", error: "File not found" };
669
668
  }
670
669
  const sizeCheck = checkFileSize(mediaPath);
671
670
  if (!sizeCheck.ok) {
@@ -687,8 +686,8 @@ async function sendDocumentFromLocal(ctx: MediaTargetContext, mediaPath: string,
687
686
  const r = await sendGroupFileMessage(token, ctx.targetId, fileBase64, undefined, ctx.replyToId, fileName);
688
687
  return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
689
688
  } else {
690
- const r = await sendChannelMessage(token, ctx.targetId, MSG.FILE_CHANNEL_UNSUPPORTED, ctx.replyToId);
691
- return { channel: "qqbot", messageId: r.id, timestamp: r.timestamp };
689
+ console.log(`${prefix} sendDocument: file not supported in channel`);
690
+ return { channel: "qqbot", error: "File not supported in channel" };
692
691
  }
693
692
  } catch (err) {
694
693
  const msg = err instanceof Error ? err.message : String(err);
@@ -20,7 +20,7 @@ import { getQQBotDataDir } from "./utils/platform.js";
20
20
  // ============ 存储的消息摘要 ============
21
21
 
22
22
  export interface RefIndexEntry {
23
- /** 消息文本内容摘要 */
23
+ /** 消息文本内容(完整保存) */
24
24
  content: string;
25
25
  /** 发送者 ID */
26
26
  senderId: string;
@@ -56,7 +56,6 @@ export interface RefAttachmentSummary {
56
56
 
57
57
  const STORAGE_DIR = getQQBotDataDir("data");
58
58
  const REF_INDEX_FILE = path.join(STORAGE_DIR, "ref-index.jsonl");
59
- const MAX_CONTENT_LENGTH = 500; // 存储的消息内容最大字符数
60
59
  const MAX_ENTRIES = 50000; // 内存中最大缓存条目数
61
60
  const TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 天
62
61
  const COMPACT_THRESHOLD_RATIO = 2; // 文件行数超过有效条目 N 倍时 compact
@@ -234,7 +233,7 @@ export function setRefIndex(refIdx: string, entry: RefIndexEntry): void {
234
233
 
235
234
  const now = Date.now();
236
235
  store.set(refIdx, {
237
- content: entry.content.slice(0, MAX_CONTENT_LENGTH),
236
+ content: entry.content,
238
237
  senderId: entry.senderId,
239
238
  senderName: entry.senderName,
240
239
  timestamp: entry.timestamp,
@@ -247,7 +246,7 @@ export function setRefIndex(refIdx: string, entry: RefIndexEntry): void {
247
246
  appendLine({
248
247
  k: refIdx,
249
248
  v: {
250
- content: entry.content.slice(0, MAX_CONTENT_LENGTH),
249
+ content: entry.content,
251
250
  senderId: entry.senderId,
252
251
  senderName: entry.senderName,
253
252
  timestamp: entry.timestamp,
@@ -1,73 +1,7 @@
1
1
  /**
2
- * 用户面向的提示文案集中管理
2
+ * 用户面向的提示文案 — 已清空
3
3
  *
4
- * 设计原则(参考 Telegram / Discord / Slack / 飞书):
5
- * 1. 禁止暴露服务器路径、原始 Error、配置文件结构
6
- * 2. 错误信息分级:用户只看到通用友好文案,技术细节走日志
7
- * 3. 统一风格:去掉 [QQBot] 前缀和 [方括号] 格式
8
- * 4. 所有面向用户的文案集中在此文件,便于维护和国际化
4
+ * 设计原则(对齐飞书插件):
5
+ * QQBot 插件层不生成额外的用户提示信息。
6
+ * 所有运行时错误仅写日志,不面向用户展示。
9
7
  */
10
-
11
- // ============ 媒体发送错误 ============
12
-
13
- export const MSG = {
14
- // 通用错误
15
- GENERIC_ERROR: "抱歉,处理消息时遇到了问题,请稍后再试~",
16
- AI_AUTH_ERROR: "抱歉,AI 服务暂时不可用,请联系管理员检查配置~",
17
- AI_PROCESS_ERROR: "抱歉,AI 处理遇到了问题,请稍后再试~",
18
- TIMEOUT_HINT: "已收到消息,正在处理中…",
19
-
20
- // 图片
21
- IMAGE_NOT_FOUND: "抱歉,图片不存在或已失效,无法发送~",
22
- IMAGE_FORMAT_UNSUPPORTED: (ext: string) => `抱歉,暂不支持 ${ext} 格式的图片~`,
23
- IMAGE_SEND_FAILED: "抱歉,图片发送失败了,请稍后再试~",
24
- IMAGE_UPLOADING: (size: string) => `正在上传图片 (${size})...`,
25
-
26
- // 语音
27
- VOICE_GENERATE_FAILED: "抱歉,语音生成失败,请稍后重试~",
28
- VOICE_CONVERT_FAILED: "抱歉,语音格式转换失败,请稍后重试~",
29
- VOICE_SEND_FAILED: "抱歉,语音发送失败了,请稍后再试~",
30
- VOICE_NOT_AVAILABLE: "抱歉,语音功能暂未开启~",
31
- VOICE_MISSING_TEXT: "抱歉,语音消息缺少内容~",
32
- VOICE_CHANNEL_UNSUPPORTED: "抱歉,语音消息暂不支持在频道中发送~",
33
-
34
- // 视频
35
- VIDEO_NOT_FOUND: "抱歉,视频文件不存在或已失效,无法发送~",
36
- VIDEO_SEND_FAILED: "抱歉,视频发送失败了,请稍后再试~",
37
- VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~",
38
- VIDEO_CHANNEL_UNSUPPORTED: "抱歉,视频消息暂不支持在频道中发送~",
39
- VIDEO_UPLOADING: (size: string) => `正在上传视频 (${size})...`,
40
-
41
- // 文件
42
- FILE_NOT_FOUND: "抱歉,文件不存在或已失效,无法发送~",
43
- FILE_SEND_FAILED: "抱歉,文件发送失败了,请稍后再试~",
44
- FILE_MISSING_PATH: "抱歉,文件消息缺少内容~",
45
- FILE_CHANNEL_UNSUPPORTED: "抱歉,文件消息暂不支持在频道中发送~",
46
- FILE_UPLOADING: (name: string, size: string) => `正在上传文件 ${name} (${size})...`,
47
-
48
- // 载荷解析
49
- PAYLOAD_PARSE_ERROR: "抱歉,消息格式异常,无法处理~",
50
- UNSUPPORTED_MEDIA_TYPE: "抱歉,暂不支持该媒体类型~",
51
- UNSUPPORTED_PAYLOAD_TYPE: "抱歉,暂不支持该消息类型~",
52
- } as const;
53
-
54
- /**
55
- * 将媒体上传/发送错误转为对用户友好的提示文案
56
- * 技术细节不暴露给用户,仅记录到日志
57
- */
58
- export function formatMediaErrorMessage(mediaType: string, err: unknown): string {
59
- const msg = err instanceof Error ? err.message : String(err);
60
- if (msg.includes("上传超时") || msg.includes("timeout") || msg.includes("Timeout")) {
61
- return `抱歉,${mediaType}资源加载超时,可能是网络原因或文件太大,请稍后再试~`;
62
- }
63
- if (msg.includes("文件不存在") || msg.includes("not found") || msg.includes("Not Found")) {
64
- return `抱歉,${mediaType}文件不存在或已失效,无法发送~`;
65
- }
66
- if (msg.includes("文件大小") || msg.includes("too large") || msg.includes("exceed")) {
67
- return `抱歉,${mediaType}文件太大了,超出了发送限制~`;
68
- }
69
- if (msg.includes("Network error") || msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND")) {
70
- return `抱歉,网络连接异常,${mediaType}发送失败,请稍后再试~`;
71
- }
72
- return `抱歉,${mediaType}发送失败了,请稍后再试~`;
73
- }