@tencent-connect/openclaw-qqbot 1.6.2-alpha.0 → 1.6.2-alpha.2

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.
@@ -52,7 +52,7 @@ export const qqbotPlugin = {
52
52
  * blockStreaming: true 表示该 Channel 支持块流式
53
53
  * 框架会收集流式响应,然后通过 deliver 回调发送
54
54
  */
55
- blockStreaming: true,
55
+ blockStreaming: false,
56
56
  },
57
57
  reload: { configPrefixes: ["channels.qqbot"] },
58
58
  // CLI onboarding wizard
@@ -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 } from "./update-checker.js";
10
+ import { triggerUpdateCheck, onUpdateFound, formatUpdateNotice } 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,7 +15,7 @@ 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";
18
+ import { MSG } from "./user-messages.js";
19
19
  import { sendPhoto, sendVoice, sendVideoMsg, sendDocument, sendMedia as sendMediaAuto } from "./outbound.js";
20
20
  function resolveSTTConfig(cfg) {
21
21
  const c = cfg;
@@ -172,7 +172,6 @@ function parseFaceTags(text) {
172
172
  }
173
173
  });
174
174
  }
175
- // formatMediaErrorMessage 已移至 user-messages.ts 集中管理
176
175
  // ============ 内部标记过滤 ============
177
176
  /**
178
177
  * 过滤内部标记(如 [[reply_to: xxx]])
@@ -315,8 +314,36 @@ export async function startGateway(ctx) {
315
314
  log?.info(`[qqbot:${account.accountId}] ${w}`);
316
315
  }
317
316
  }
318
- // 后台版本检查(供 /qqbot-version、/qqbot-upgrade 指令被动查询)
317
+ // 后台版本检查(detached 子进程,零阻塞)
319
318
  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
+ });
320
347
  // 初始化 API 配置(markdown 支持)
321
348
  initApiConfig({
322
349
  markdownSupport: account.markdownSupport,
@@ -1502,7 +1529,6 @@ export async function startGateway(ctx) {
1502
1529
  const result = await sendPhoto(mediaTarget, item.content);
1503
1530
  if (result.error) {
1504
1531
  log?.error(`[qqbot:${account.accountId}] sendPhoto error: ${result.error}`);
1505
- await sendErrorMessage(formatMediaErrorMessage("图片", new Error(result.error)));
1506
1532
  }
1507
1533
  }
1508
1534
  else if (item.type === "voice") {
@@ -1517,26 +1543,22 @@ export async function startGateway(ctx) {
1517
1543
  ]);
1518
1544
  if (result.error) {
1519
1545
  log?.error(`[qqbot:${account.accountId}] sendVoice error: ${result.error}`);
1520
- await sendErrorMessage(formatMediaErrorMessage("语音", new Error(result.error)));
1521
1546
  }
1522
1547
  }
1523
1548
  catch (err) {
1524
1549
  log?.error(`[qqbot:${account.accountId}] sendVoice unexpected error: ${err}`);
1525
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1526
1550
  }
1527
1551
  }
1528
1552
  else if (item.type === "video") {
1529
1553
  const result = await sendVideoMsg(mediaTarget, item.content);
1530
1554
  if (result.error) {
1531
1555
  log?.error(`[qqbot:${account.accountId}] sendVideoMsg error: ${result.error}`);
1532
- await sendErrorMessage(formatMediaErrorMessage("视频", new Error(result.error)));
1533
1556
  }
1534
1557
  }
1535
1558
  else if (item.type === "file") {
1536
1559
  const result = await sendDocument(mediaTarget, item.content);
1537
1560
  if (result.error) {
1538
1561
  log?.error(`[qqbot:${account.accountId}] sendDocument error: ${result.error}`);
1539
- await sendErrorMessage(formatMediaErrorMessage("文件", new Error(result.error)));
1540
1562
  }
1541
1563
  }
1542
1564
  else if (item.type === "media") {
@@ -1551,7 +1573,6 @@ export async function startGateway(ctx) {
1551
1573
  });
1552
1574
  if (result.error) {
1553
1575
  log?.error(`[qqbot:${account.accountId}] sendMedia(auto) error: ${result.error}`);
1554
- await sendErrorMessage(formatMediaErrorMessage("媒体", new Error(result.error)));
1555
1576
  }
1556
1577
  }
1557
1578
  }
@@ -1620,12 +1641,12 @@ export async function startGateway(ctx) {
1620
1641
  if (parsedPayload.source === "file") {
1621
1642
  try {
1622
1643
  if (!(await fileExistsAsync(imageUrl))) {
1623
- await sendErrorMessage(MSG.IMAGE_NOT_FOUND);
1644
+ log?.error(`[qqbot:${account.accountId}] Image not found: ${imageUrl}`);
1624
1645
  return;
1625
1646
  }
1626
1647
  const imgSzCheck = checkFileSize(imageUrl);
1627
1648
  if (!imgSzCheck.ok) {
1628
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1649
+ log?.error(`[qqbot:${account.accountId}] Image size check failed: ${imgSzCheck.error}`);
1629
1650
  return;
1630
1651
  }
1631
1652
  const fileBuffer = await readFileAsync(imageUrl);
@@ -1649,7 +1670,6 @@ export async function startGateway(ctx) {
1649
1670
  }
1650
1671
  catch (readErr) {
1651
1672
  log?.error(`[qqbot:${account.accountId}] Failed to read local image: ${readErr}`);
1652
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1653
1673
  return;
1654
1674
  }
1655
1675
  }
@@ -1685,7 +1705,6 @@ export async function startGateway(ctx) {
1685
1705
  }
1686
1706
  catch (err) {
1687
1707
  log?.error(`[qqbot:${account.accountId}] Failed to send image: ${err}`);
1688
- await sendErrorMessage(formatMediaErrorMessage("图片", err));
1689
1708
  }
1690
1709
  }
1691
1710
  else if (parsedPayload.mediaType === "audio") {
@@ -1699,7 +1718,6 @@ export async function startGateway(ctx) {
1699
1718
  const ttsCfg = resolveTTSConfig(cfg);
1700
1719
  if (!ttsCfg) {
1701
1720
  log?.error(`[qqbot:${account.accountId}] TTS not configured (channels.qqbot.tts in openclaw.json)`);
1702
- await sendErrorMessage(MSG.VOICE_NOT_AVAILABLE);
1703
1721
  }
1704
1722
  else {
1705
1723
  log?.info(`[qqbot:${account.accountId}] TTS: "${ttsText.slice(0, 50)}..." via ${ttsCfg.model}`);
@@ -1723,7 +1741,6 @@ export async function startGateway(ctx) {
1723
1741
  }
1724
1742
  catch (err) {
1725
1743
  log?.error(`[qqbot:${account.accountId}] TTS/voice send failed: ${err}`);
1726
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1727
1744
  }
1728
1745
  }
1729
1746
  else if (parsedPayload.mediaType === "video") {
@@ -1791,7 +1808,6 @@ export async function startGateway(ctx) {
1791
1808
  }
1792
1809
  catch (err) {
1793
1810
  log?.error(`[qqbot:${account.accountId}] Video send failed: ${err}`);
1794
- await sendErrorMessage(formatMediaErrorMessage("视频", err));
1795
1811
  }
1796
1812
  }
1797
1813
  else if (parsedPayload.mediaType === "file") {
@@ -1843,7 +1859,6 @@ export async function startGateway(ctx) {
1843
1859
  }
1844
1860
  catch (err) {
1845
1861
  log?.error(`[qqbot:${account.accountId}] File send failed: ${err}`);
1846
- await sendErrorMessage(formatMediaErrorMessage("文件", err));
1847
1862
  }
1848
1863
  }
1849
1864
  else {
@@ -2196,10 +2211,10 @@ export async function startGateway(ctx) {
2196
2211
  // 发送错误提示给用户,显示完整错误信息
2197
2212
  const errMsg = String(err);
2198
2213
  if (errMsg.includes("401") || errMsg.includes("key") || errMsg.includes("auth")) {
2199
- await sendErrorMessage(MSG.AI_AUTH_ERROR);
2214
+ log?.error(`[qqbot:${account.accountId}] AI auth error: ${errMsg}`);
2200
2215
  }
2201
2216
  else {
2202
- await sendErrorMessage(MSG.AI_PROCESS_ERROR);
2217
+ log?.error(`[qqbot:${account.accountId}] AI process error: ${errMsg}`);
2203
2218
  }
2204
2219
  },
2205
2220
  },
@@ -2217,7 +2232,6 @@ export async function startGateway(ctx) {
2217
2232
  }
2218
2233
  if (!hasResponse) {
2219
2234
  log?.error(`[qqbot:${account.accountId}] No response within timeout`);
2220
- await sendErrorMessage(MSG.TIMEOUT_HINT);
2221
2235
  }
2222
2236
  }
2223
2237
  finally {
@@ -2236,7 +2250,6 @@ export async function startGateway(ctx) {
2236
2250
  }
2237
2251
  catch (err) {
2238
2252
  log?.error(`[qqbot:${account.accountId}] Message processing failed: ${err}`);
2239
- await sendErrorMessage(MSG.GENERIC_ERROR);
2240
2253
  }
2241
2254
  };
2242
2255
  ws.on("open", () => {
@@ -208,7 +208,7 @@ export async function sendPhoto(ctx, imagePath) {
208
208
  let imageUrl = mediaPath;
209
209
  if (isLocal) {
210
210
  if (!(await fileExistsAsync(mediaPath))) {
211
- return { channel: "qqbot", error: MSG.IMAGE_NOT_FOUND };
211
+ return { channel: "qqbot", error: "图片不存在或已失效" };
212
212
  }
213
213
  const sizeCheck = checkFileSize(mediaPath);
214
214
  if (!sizeCheck.ok) {
@@ -345,7 +345,7 @@ async function sendVoiceFromLocal(ctx, mediaPath, directUploadFormats, transcode
345
345
  // 等待文件就绪(TTS 异步生成,文件可能还没写完)
346
346
  const fileSize = await waitForFile(mediaPath);
347
347
  if (fileSize === 0) {
348
- return { channel: "qqbot", error: MSG.VOICE_GENERATE_FAILED };
348
+ return { channel: "qqbot", error: "语音文件未就绪" };
349
349
  }
350
350
  // 精细检测:是否需要转码
351
351
  const needsTranscode = shouldTranscodeVoice(mediaPath);
@@ -441,7 +441,7 @@ export async function sendVideoMsg(ctx, videoPath) {
441
441
  /** 从本地文件发送视频(sendVideoMsg 的内部辅助) */
442
442
  async function sendVideoFromLocal(ctx, mediaPath, prefix) {
443
443
  if (!(await fileExistsAsync(mediaPath))) {
444
- return { channel: "qqbot", error: MSG.VIDEO_NOT_FOUND };
444
+ return { channel: "qqbot", error: "视频文件不存在或已失效" };
445
445
  }
446
446
  const sizeCheck = checkFileSize(mediaPath);
447
447
  if (!sizeCheck.ok) {
@@ -528,7 +528,7 @@ export async function sendDocument(ctx, filePath) {
528
528
  async function sendDocumentFromLocal(ctx, mediaPath, prefix) {
529
529
  const fileName = sanitizeFileName(path.basename(mediaPath));
530
530
  if (!(await fileExistsAsync(mediaPath))) {
531
- return { channel: "qqbot", error: MSG.FILE_NOT_FOUND };
531
+ return { channel: "qqbot", error: "文件不存在或已失效" };
532
532
  }
533
533
  const sizeCheck = checkFileSize(mediaPath);
534
534
  if (!sizeCheck.ok) {
@@ -12,9 +12,16 @@ 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;
15
20
  export declare function triggerUpdateCheck(log?: {
16
21
  info: (msg: string) => void;
17
22
  error: (msg: string) => void;
18
23
  debug?: (msg: string) => void;
19
24
  }): void;
20
25
  export declare function getUpdateInfo(): UpdateInfo;
26
+ export declare function formatUpdateNotice(info: UpdateInfo): string;
27
+ export {};
@@ -24,6 +24,15 @@ let _lastInfo = {
24
24
  checkedAt: 0,
25
25
  };
26
26
  let _checking = false;
27
+ /** 已通知过的版本号,避免同一版本重复推送 */
28
+ let _notifiedVersion = null;
29
+ let _onUpdateFound = null;
30
+ /**
31
+ * 注册新版本发现回调(仅在首次检测到某个新版本时触发一次)
32
+ */
33
+ export function onUpdateFound(cb) {
34
+ _onUpdateFound = cb;
35
+ }
27
36
  export function triggerUpdateCheck(log) {
28
37
  if (_checking)
29
38
  return;
@@ -55,6 +64,11 @@ export function triggerUpdateCheck(log) {
55
64
  _lastInfo = { current: CURRENT_VERSION, latest: compareTarget, hasUpdate, checkedAt: now };
56
65
  if (hasUpdate) {
57
66
  log?.info?.(`[qqbot:update-checker] new version available: ${compareTarget} (current: ${CURRENT_VERSION})`);
67
+ // 首次发现该版本时触发回调
68
+ if (_onUpdateFound && compareTarget !== _notifiedVersion) {
69
+ _notifiedVersion = compareTarget;
70
+ _onUpdateFound(_lastInfo);
71
+ }
58
72
  }
59
73
  }
60
74
  catch (parseErr) {
@@ -65,6 +79,11 @@ export function triggerUpdateCheck(log) {
65
79
  export function getUpdateInfo() {
66
80
  return { ..._lastInfo };
67
81
  }
82
+ export function formatUpdateNotice(info) {
83
+ if (!info.hasUpdate || !info.latest)
84
+ return "";
85
+ return `\u{1f195} 有新版本可用: v${info.latest}(当前 v${info.current})\n使用 /qqbot-upgrade 升级`;
86
+ }
68
87
  function compareVersions(a, b) {
69
88
  const parse = (v) => {
70
89
  const clean = v.replace(/^v/, "");
@@ -1,43 +1,16 @@
1
1
  /**
2
2
  * 用户面向的提示文案集中管理
3
- *
4
- * 设计原则(参考 Telegram / Discord / Slack / 飞书):
5
- * 1. 禁止暴露服务器路径、原始 Error、配置文件结构
6
- * 2. 错误信息分级:用户只看到通用友好文案,技术细节走日志
7
- * 3. 统一风格:去掉 [QQBot] 前缀和 [方括号] 格式
8
- * 4. 所有面向用户的文案集中在此文件,便于维护和国际化
3
+ * 仅保留格式校验类提示,运行时错误走日志不面向用户
9
4
  */
10
5
  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
6
  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
7
  readonly VOICE_CHANNEL_UNSUPPORTED: "抱歉,语音消息暂不支持在频道中发送~";
25
- readonly VIDEO_NOT_FOUND: "抱歉,视频文件不存在或已失效,无法发送~";
26
- readonly VIDEO_SEND_FAILED: "抱歉,视频发送失败了,请稍后再试~";
27
- readonly VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~";
28
8
  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
9
  readonly FILE_CHANNEL_UNSUPPORTED: "抱歉,文件消息暂不支持在频道中发送~";
34
- readonly FILE_UPLOADING: (name: string, size: string) => string;
10
+ readonly VOICE_MISSING_TEXT: "抱歉,语音消息缺少内容~";
11
+ readonly VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~";
12
+ readonly FILE_MISSING_PATH: "抱歉,文件消息缺少内容~";
35
13
  readonly PAYLOAD_PARSE_ERROR: "抱歉,消息格式异常,无法处理~";
36
14
  readonly UNSUPPORTED_MEDIA_TYPE: "抱歉,暂不支持该媒体类型~";
37
15
  readonly UNSUPPORTED_PAYLOAD_TYPE: "抱歉,暂不支持该消息类型~";
38
16
  };
39
- /**
40
- * 将媒体上传/发送错误转为对用户友好的提示文案
41
- * 技术细节不暴露给用户,仅记录到日志
42
- */
43
- export declare function formatMediaErrorMessage(mediaType: string, err: unknown): string;
@@ -1,65 +1,20 @@
1
1
  /**
2
2
  * 用户面向的提示文案集中管理
3
- *
4
- * 设计原则(参考 Telegram / Discord / Slack / 飞书):
5
- * 1. 禁止暴露服务器路径、原始 Error、配置文件结构
6
- * 2. 错误信息分级:用户只看到通用友好文案,技术细节走日志
7
- * 3. 统一风格:去掉 [QQBot] 前缀和 [方括号] 格式
8
- * 4. 所有面向用户的文案集中在此文件,便于维护和国际化
3
+ * 仅保留格式校验类提示,运行时错误走日志不面向用户
9
4
  */
10
- // ============ 媒体发送错误 ============
11
5
  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: "抱歉,图片不存在或已失效,无法发送~",
6
+ // 图片格式校验
19
7
  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: "抱歉,语音消息缺少内容~",
8
+ // 频道不支持的媒体类型
28
9
  VOICE_CHANNEL_UNSUPPORTED: "抱歉,语音消息暂不支持在频道中发送~",
29
- // 视频
30
- VIDEO_NOT_FOUND: "抱歉,视频文件不存在或已失效,无法发送~",
31
- VIDEO_SEND_FAILED: "抱歉,视频发送失败了,请稍后再试~",
32
- VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~",
33
10
  VIDEO_CHANNEL_UNSUPPORTED: "抱歉,视频消息暂不支持在频道中发送~",
34
- VIDEO_UPLOADING: (size) => `正在上传视频 (${size})...`,
35
- // 文件
36
- FILE_NOT_FOUND: "抱歉,文件不存在或已失效,无法发送~",
37
- FILE_SEND_FAILED: "抱歉,文件发送失败了,请稍后再试~",
38
- FILE_MISSING_PATH: "抱歉,文件消息缺少内容~",
39
11
  FILE_CHANNEL_UNSUPPORTED: "抱歉,文件消息暂不支持在频道中发送~",
40
- FILE_UPLOADING: (name, size) => `正在上传文件 ${name} (${size})...`,
41
- // 载荷解析
12
+ // 缺少必要内容
13
+ VOICE_MISSING_TEXT: "抱歉,语音消息缺少内容~",
14
+ VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~",
15
+ FILE_MISSING_PATH: "抱歉,文件消息缺少内容~",
16
+ // 载荷解析校验
42
17
  PAYLOAD_PARSE_ERROR: "抱歉,消息格式异常,无法处理~",
43
18
  UNSUPPORTED_MEDIA_TYPE: "抱歉,暂不支持该媒体类型~",
44
19
  UNSUPPORTED_PAYLOAD_TYPE: "抱歉,暂不支持该消息类型~",
45
20
  };
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.0",
3
+ "version": "1.6.2-alpha.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/channel.ts CHANGED
@@ -66,7 +66,7 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
66
66
  * blockStreaming: true 表示该 Channel 支持块流式
67
67
  * 框架会收集流式响应,然后通过 deliver 回调发送
68
68
  */
69
- blockStreaming: true,
69
+ blockStreaming: false,
70
70
  },
71
71
  reload: { configPrefixes: ["channels.qqbot"] },
72
72
  // CLI onboarding wizard
package/src/gateway.ts CHANGED
@@ -8,7 +8,7 @@ import { recordKnownUser, flushKnownUsers, listKnownUsers } from "./known-users.
8
8
  import { getQQBotRuntime } from "./runtime.js";
9
9
  import { setRefIndex, getRefIndex, formatRefEntryForAgent, flushRefIndex, type RefAttachmentSummary } from "./ref-index-store.js";
10
10
  import { matchSlashCommand, getPluginVersion, type SlashCommandContext, type SlashCommandFileResult, type QueueSnapshot } from "./slash-commands.js";
11
- import { triggerUpdateCheck } from "./update-checker.js";
11
+ import { triggerUpdateCheck, onUpdateFound, formatUpdateNotice } from "./update-checker.js";
12
12
  import { startImageServer, isImageServerRunning, downloadFile, type ImageServerConfig } from "./image-server.js";
13
13
  import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize, DEFAULT_IMAGE_SIZE } from "./utils/image-size.js";
14
14
  import { parseQQBotPayload, encodePayloadForCron, isCronReminderPayload, isMediaPayload, type CronReminderPayload, type MediaPayload } from "./utils/payload.js";
@@ -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
+ import { MSG } from "./user-messages.js";
20
20
  import { sendPhoto, sendVoice, sendVideoMsg, sendDocument, sendMedia as sendMediaAuto, type MediaTargetContext } from "./outbound.js";
21
21
 
22
22
  /**
@@ -221,8 +221,6 @@ function parseFaceTags(text: string): string {
221
221
  });
222
222
  }
223
223
 
224
- // formatMediaErrorMessage 已移至 user-messages.ts 集中管理
225
-
226
224
  // ============ 内部标记过滤 ============
227
225
 
228
226
  /**
@@ -410,9 +408,36 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
410
408
  }
411
409
  }
412
410
 
413
- // 后台版本检查(供 /qqbot-version、/qqbot-upgrade 指令被动查询)
411
+ // 后台版本检查(detached 子进程,零阻塞)
414
412
  triggerUpdateCheck(log);
415
413
 
414
+ // 注册新版本通知回调:仅发给管理员,带防抖
415
+ let lastUpdateNotifyAt = 0;
416
+ const UPDATE_NOTIFY_DEBOUNCE_MS = 5 * 60 * 1000; // 5 分钟内不重复通知
417
+ onUpdateFound(async (info) => {
418
+ try {
419
+ // 防抖:避免短时间内重复推送
420
+ const now = Date.now();
421
+ if (now - lastUpdateNotifyAt < UPDATE_NOTIFY_DEBOUNCE_MS) {
422
+ log?.debug?.(`[qqbot:${account.accountId}] Update notification debounced`);
423
+ return;
424
+ }
425
+ const notice = formatUpdateNotice(info);
426
+ if (!notice) return;
427
+ const adminId = resolveAdminOpenId();
428
+ if (!adminId) {
429
+ log?.debug?.(`[qqbot:${account.accountId}] No admin or known user to send update notification`);
430
+ return;
431
+ }
432
+ const token = await getAccessToken(account.appId, account.clientSecret);
433
+ await sendProactiveC2CMessage(token, adminId, notice);
434
+ lastUpdateNotifyAt = Date.now();
435
+ log?.info(`[qqbot:${account.accountId}] Sent update notification to admin: ${adminId}`);
436
+ } catch (err) {
437
+ log?.debug?.(`[qqbot:${account.accountId}] Failed to send update notification to admin: ${err}`);
438
+ }
439
+ });
440
+
416
441
  // 初始化 API 配置(markdown 支持)
417
442
  initApiConfig({
418
443
  markdownSupport: account.markdownSupport,
@@ -1678,7 +1703,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1678
1703
  const result = await sendPhoto(mediaTarget, item.content);
1679
1704
  if (result.error) {
1680
1705
  log?.error(`[qqbot:${account.accountId}] sendPhoto error: ${result.error}`);
1681
- await sendErrorMessage(formatMediaErrorMessage("图片", new Error(result.error)));
1682
1706
  }
1683
1707
  } else if (item.type === "voice") {
1684
1708
  const uploadFormats = account.config?.audioFormatPolicy?.uploadDirectFormats ?? account.config?.voiceDirectUploadFormats;
@@ -1694,23 +1718,19 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1694
1718
  ]);
1695
1719
  if (result.error) {
1696
1720
  log?.error(`[qqbot:${account.accountId}] sendVoice error: ${result.error}`);
1697
- await sendErrorMessage(formatMediaErrorMessage("语音", new Error(result.error)));
1698
1721
  }
1699
1722
  } catch (err) {
1700
1723
  log?.error(`[qqbot:${account.accountId}] sendVoice unexpected error: ${err}`);
1701
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1702
1724
  }
1703
1725
  } else if (item.type === "video") {
1704
1726
  const result = await sendVideoMsg(mediaTarget, item.content);
1705
1727
  if (result.error) {
1706
1728
  log?.error(`[qqbot:${account.accountId}] sendVideoMsg error: ${result.error}`);
1707
- await sendErrorMessage(formatMediaErrorMessage("视频", new Error(result.error)));
1708
1729
  }
1709
1730
  } else if (item.type === "file") {
1710
1731
  const result = await sendDocument(mediaTarget, item.content);
1711
1732
  if (result.error) {
1712
1733
  log?.error(`[qqbot:${account.accountId}] sendDocument error: ${result.error}`);
1713
- await sendErrorMessage(formatMediaErrorMessage("文件", new Error(result.error)));
1714
1734
  }
1715
1735
  } else if (item.type === "media") {
1716
1736
  // qqmedia: 自动根据扩展名路由到 sendPhoto/sendVoice/sendVideoMsg/sendDocument
@@ -1724,7 +1744,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1724
1744
  });
1725
1745
  if (result.error) {
1726
1746
  log?.error(`[qqbot:${account.accountId}] sendMedia(auto) error: ${result.error}`);
1727
- await sendErrorMessage(formatMediaErrorMessage("媒体", new Error(result.error)));
1728
1747
  }
1729
1748
  }
1730
1749
  }
@@ -1799,12 +1818,12 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1799
1818
  if (parsedPayload.source === "file") {
1800
1819
  try {
1801
1820
  if (!(await fileExistsAsync(imageUrl))) {
1802
- await sendErrorMessage(MSG.IMAGE_NOT_FOUND);
1821
+ log?.error(`[qqbot:${account.accountId}] Image not found: ${imageUrl}`);
1803
1822
  return;
1804
1823
  }
1805
1824
  const imgSzCheck = checkFileSize(imageUrl);
1806
1825
  if (!imgSzCheck.ok) {
1807
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1826
+ log?.error(`[qqbot:${account.accountId}] Image size check failed: ${imgSzCheck.error}`);
1808
1827
  return;
1809
1828
  }
1810
1829
  const fileBuffer = await readFileAsync(imageUrl);
@@ -1827,7 +1846,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1827
1846
  log?.info(`[qqbot:${account.accountId}] Converted local image to Base64 (size: ${formatFileSize(fileBuffer.length)})`);
1828
1847
  } catch (readErr) {
1829
1848
  log?.error(`[qqbot:${account.accountId}] Failed to read local image: ${readErr}`);
1830
- await sendErrorMessage(MSG.IMAGE_SEND_FAILED);
1831
1849
  return;
1832
1850
  }
1833
1851
  }
@@ -1860,7 +1878,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1860
1878
  }
1861
1879
  } catch (err) {
1862
1880
  log?.error(`[qqbot:${account.accountId}] Failed to send image: ${err}`);
1863
- await sendErrorMessage(formatMediaErrorMessage("图片", err));
1864
1881
  }
1865
1882
  } else if (parsedPayload.mediaType === "audio") {
1866
1883
  // TTS 语音发送:文字 → PCM → SILK → QQ 语音
@@ -1872,7 +1889,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1872
1889
  const ttsCfg = resolveTTSConfig(cfg as Record<string, unknown>);
1873
1890
  if (!ttsCfg) {
1874
1891
  log?.error(`[qqbot:${account.accountId}] TTS not configured (channels.qqbot.tts in openclaw.json)`);
1875
- await sendErrorMessage(MSG.VOICE_NOT_AVAILABLE);
1876
1892
  } else {
1877
1893
  log?.info(`[qqbot:${account.accountId}] TTS: "${ttsText.slice(0, 50)}..." via ${ttsCfg.model}`);
1878
1894
  const ttsDir = getQQBotDataDir("tts");
@@ -1893,7 +1909,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1893
1909
  }
1894
1910
  } catch (err) {
1895
1911
  log?.error(`[qqbot:${account.accountId}] TTS/voice send failed: ${err}`);
1896
- await sendErrorMessage(formatMediaErrorMessage("语音", err));
1897
1912
  }
1898
1913
  } else if (parsedPayload.mediaType === "video") {
1899
1914
  // 视频发送:支持公网 URL 和本地文件
@@ -1954,7 +1969,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1954
1969
  }
1955
1970
  } catch (err) {
1956
1971
  log?.error(`[qqbot:${account.accountId}] Video send failed: ${err}`);
1957
- await sendErrorMessage(formatMediaErrorMessage("视频", err));
1958
1972
  }
1959
1973
  } else if (parsedPayload.mediaType === "file") {
1960
1974
  // 文件发送
@@ -1999,7 +2013,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1999
2013
  }
2000
2014
  } catch (err) {
2001
2015
  log?.error(`[qqbot:${account.accountId}] File send failed: ${err}`);
2002
- await sendErrorMessage(formatMediaErrorMessage("文件", err));
2003
2016
  }
2004
2017
  } else {
2005
2018
  log?.error(`[qqbot:${account.accountId}] Unknown media type: ${(parsedPayload as MediaPayload).mediaType}`);
@@ -2359,9 +2372,9 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2359
2372
  // 发送错误提示给用户,显示完整错误信息
2360
2373
  const errMsg = String(err);
2361
2374
  if (errMsg.includes("401") || errMsg.includes("key") || errMsg.includes("auth")) {
2362
- await sendErrorMessage(MSG.AI_AUTH_ERROR);
2375
+ log?.error(`[qqbot:${account.accountId}] AI auth error: ${errMsg}`);
2363
2376
  } else {
2364
- await sendErrorMessage(MSG.AI_PROCESS_ERROR);
2377
+ log?.error(`[qqbot:${account.accountId}] AI process error: ${errMsg}`);
2365
2378
  }
2366
2379
  },
2367
2380
  },
@@ -2379,7 +2392,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2379
2392
  }
2380
2393
  if (!hasResponse) {
2381
2394
  log?.error(`[qqbot:${account.accountId}] No response within timeout`);
2382
- await sendErrorMessage(MSG.TIMEOUT_HINT);
2383
2395
  }
2384
2396
  } finally {
2385
2397
  // 清理 tool-only 兜底定时器
@@ -2396,7 +2408,6 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2396
2408
  }
2397
2409
  } catch (err) {
2398
2410
  log?.error(`[qqbot:${account.accountId}] Message processing failed: ${err}`);
2399
- await sendErrorMessage(MSG.GENERIC_ERROR);
2400
2411
  }
2401
2412
  };
2402
2413
 
package/src/outbound.ts CHANGED
@@ -313,7 +313,7 @@ export async function sendPhoto(
313
313
 
314
314
  if (isLocal) {
315
315
  if (!(await fileExistsAsync(mediaPath))) {
316
- return { channel: "qqbot", error: MSG.IMAGE_NOT_FOUND };
316
+ return { channel: "qqbot", error: "图片不存在或已失效" };
317
317
  }
318
318
  const sizeCheck = checkFileSize(mediaPath);
319
319
  if (!sizeCheck.ok) {
@@ -464,7 +464,7 @@ async function sendVoiceFromLocal(
464
464
  // 等待文件就绪(TTS 异步生成,文件可能还没写完)
465
465
  const fileSize = await waitForFile(mediaPath);
466
466
  if (fileSize === 0) {
467
- return { channel: "qqbot", error: MSG.VOICE_GENERATE_FAILED };
467
+ return { channel: "qqbot", error: "语音文件未就绪" };
468
468
  }
469
469
 
470
470
  // 精细检测:是否需要转码
@@ -570,7 +570,7 @@ export async function sendVideoMsg(
570
570
  /** 从本地文件发送视频(sendVideoMsg 的内部辅助) */
571
571
  async function sendVideoFromLocal(ctx: MediaTargetContext, mediaPath: string, prefix: string): Promise<OutboundResult> {
572
572
  if (!(await fileExistsAsync(mediaPath))) {
573
- return { channel: "qqbot", error: MSG.VIDEO_NOT_FOUND };
573
+ return { channel: "qqbot", error: "视频文件不存在或已失效" };
574
574
  }
575
575
  const sizeCheck = checkFileSize(mediaPath);
576
576
  if (!sizeCheck.ok) {
@@ -665,7 +665,7 @@ async function sendDocumentFromLocal(ctx: MediaTargetContext, mediaPath: string,
665
665
  const fileName = sanitizeFileName(path.basename(mediaPath));
666
666
 
667
667
  if (!(await fileExistsAsync(mediaPath))) {
668
- return { channel: "qqbot", error: MSG.FILE_NOT_FOUND };
668
+ return { channel: "qqbot", error: "文件不存在或已失效" };
669
669
  }
670
670
  const sizeCheck = checkFileSize(mediaPath);
671
671
  if (!sizeCheck.ok) {
@@ -38,6 +38,19 @@ let _lastInfo: UpdateInfo = {
38
38
 
39
39
  let _checking = false;
40
40
 
41
+ /** 已通知过的版本号,避免同一版本重复推送 */
42
+ let _notifiedVersion: string | null = null;
43
+
44
+ type UpdateFoundCallback = (info: UpdateInfo) => void;
45
+ let _onUpdateFound: UpdateFoundCallback | null = null;
46
+
47
+ /**
48
+ * 注册新版本发现回调(仅在首次检测到某个新版本时触发一次)
49
+ */
50
+ export function onUpdateFound(cb: UpdateFoundCallback): void {
51
+ _onUpdateFound = cb;
52
+ }
53
+
41
54
  export function triggerUpdateCheck(log?: {
42
55
  info: (msg: string) => void;
43
56
  error: (msg: string) => void;
@@ -77,6 +90,11 @@ export function triggerUpdateCheck(log?: {
77
90
  _lastInfo = { current: CURRENT_VERSION, latest: compareTarget, hasUpdate, checkedAt: now };
78
91
  if (hasUpdate) {
79
92
  log?.info?.(`[qqbot:update-checker] new version available: ${compareTarget} (current: ${CURRENT_VERSION})`);
93
+ // 首次发现该版本时触发回调
94
+ if (_onUpdateFound && compareTarget !== _notifiedVersion) {
95
+ _notifiedVersion = compareTarget;
96
+ _onUpdateFound(_lastInfo);
97
+ }
80
98
  }
81
99
  } catch (parseErr) {
82
100
  _lastInfo = { current: CURRENT_VERSION, latest: null, hasUpdate: false, checkedAt: now, error: String(parseErr) };
@@ -89,6 +107,11 @@ export function getUpdateInfo(): UpdateInfo {
89
107
  return { ..._lastInfo };
90
108
  }
91
109
 
110
+ export function formatUpdateNotice(info: UpdateInfo): string {
111
+ if (!info.hasUpdate || !info.latest) return "";
112
+ return `\u{1f195} 有新版本可用: v${info.latest}(当前 v${info.current})\n使用 /qqbot-upgrade 升级`;
113
+ }
114
+
92
115
  function compareVersions(a: string, b: string): number {
93
116
  const parse = (v: string) => {
94
117
  const clean = v.replace(/^v/, "");
@@ -1,73 +1,24 @@
1
1
  /**
2
2
  * 用户面向的提示文案集中管理
3
- *
4
- * 设计原则(参考 Telegram / Discord / Slack / 飞书):
5
- * 1. 禁止暴露服务器路径、原始 Error、配置文件结构
6
- * 2. 错误信息分级:用户只看到通用友好文案,技术细节走日志
7
- * 3. 统一风格:去掉 [QQBot] 前缀和 [方括号] 格式
8
- * 4. 所有面向用户的文案集中在此文件,便于维护和国际化
3
+ * 仅保留格式校验类提示,运行时错误走日志不面向用户
9
4
  */
10
5
 
11
- // ============ 媒体发送错误 ============
12
-
13
6
  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: "抱歉,图片不存在或已失效,无法发送~",
7
+ // 图片格式校验
22
8
  IMAGE_FORMAT_UNSUPPORTED: (ext: string) => `抱歉,暂不支持 ${ext} 格式的图片~`,
23
- IMAGE_SEND_FAILED: "抱歉,图片发送失败了,请稍后再试~",
24
- IMAGE_UPLOADING: (size: string) => `正在上传图片 (${size})...`,
25
9
 
26
- // 语音
27
- VOICE_GENERATE_FAILED: "抱歉,语音生成失败,请稍后重试~",
28
- VOICE_CONVERT_FAILED: "抱歉,语音格式转换失败,请稍后重试~",
29
- VOICE_SEND_FAILED: "抱歉,语音发送失败了,请稍后再试~",
30
- VOICE_NOT_AVAILABLE: "抱歉,语音功能暂未开启~",
31
- VOICE_MISSING_TEXT: "抱歉,语音消息缺少内容~",
10
+ // 频道不支持的媒体类型
32
11
  VOICE_CHANNEL_UNSUPPORTED: "抱歉,语音消息暂不支持在频道中发送~",
33
-
34
- // 视频
35
- VIDEO_NOT_FOUND: "抱歉,视频文件不存在或已失效,无法发送~",
36
- VIDEO_SEND_FAILED: "抱歉,视频发送失败了,请稍后再试~",
37
- VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~",
38
12
  VIDEO_CHANNEL_UNSUPPORTED: "抱歉,视频消息暂不支持在频道中发送~",
39
- VIDEO_UPLOADING: (size: string) => `正在上传视频 (${size})...`,
13
+ FILE_CHANNEL_UNSUPPORTED: "抱歉,文件消息暂不支持在频道中发送~",
40
14
 
41
- // 文件
42
- FILE_NOT_FOUND: "抱歉,文件不存在或已失效,无法发送~",
43
- FILE_SEND_FAILED: "抱歉,文件发送失败了,请稍后再试~",
15
+ // 缺少必要内容
16
+ VOICE_MISSING_TEXT: "抱歉,语音消息缺少内容~",
17
+ VIDEO_MISSING_PATH: "抱歉,视频消息缺少内容~",
44
18
  FILE_MISSING_PATH: "抱歉,文件消息缺少内容~",
45
- FILE_CHANNEL_UNSUPPORTED: "抱歉,文件消息暂不支持在频道中发送~",
46
- FILE_UPLOADING: (name: string, size: string) => `正在上传文件 ${name} (${size})...`,
47
19
 
48
- // 载荷解析
20
+ // 载荷解析校验
49
21
  PAYLOAD_PARSE_ERROR: "抱歉,消息格式异常,无法处理~",
50
22
  UNSUPPORTED_MEDIA_TYPE: "抱歉,暂不支持该媒体类型~",
51
23
  UNSUPPORTED_PAYLOAD_TYPE: "抱歉,暂不支持该消息类型~",
52
24
  } 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
- }