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

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.
@@ -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",
@@ -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 ? "..." : ""}`);
@@ -17,6 +17,7 @@ import { checkFileSize, readFileAsync, fileExistsAsync, formatFileSize } from ".
17
17
  import { getQQBotDataDir, isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, runDiagnostics } from "./utils/platform.js";
18
18
  import { MSG, formatMediaErrorMessage } from "./user-messages.js";
19
19
  import { sendPhoto, sendVoice, sendVideoMsg, sendDocument, sendMedia as sendMediaAuto } from "./outbound.js";
20
+ import { chunkText, TEXT_CHUNK_LIMIT } from "./channel.js";
20
21
  function resolveSTTConfig(cfg) {
21
22
  const c = cfg;
22
23
  // 优先使用 channels.qqbot.stt(插件专属配置)
@@ -1479,23 +1480,27 @@ export async function startGateway(ctx) {
1479
1480
  };
1480
1481
  for (const item of sendQueue) {
1481
1482
  if (item.type === "text") {
1482
- try {
1483
- await sendWithTokenRetry(async (token) => {
1484
- const ref = consumeQuoteRef();
1485
- if (event.type === "c2c") {
1486
- return await sendC2CMessage(token, event.senderId, item.content, event.messageId, ref);
1487
- }
1488
- else if (event.type === "group" && event.groupOpenid) {
1489
- return await sendGroupMessage(token, event.groupOpenid, item.content, event.messageId);
1490
- }
1491
- else if (event.channelId) {
1492
- return await sendChannelMessage(token, event.channelId, item.content, event.messageId);
1493
- }
1494
- });
1495
- log?.info(`[qqbot:${account.accountId}] Sent text: ${item.content.slice(0, 50)}...`);
1496
- }
1497
- catch (err) {
1498
- log?.error(`[qqbot:${account.accountId}] Failed to send text: ${err}`);
1483
+ // 对长文本进行分块发送
1484
+ const textChunks = getQQBotRuntime().channel.text.chunkMarkdownText(item.content, TEXT_CHUNK_LIMIT);
1485
+ for (const chunk of textChunks) {
1486
+ try {
1487
+ await sendWithTokenRetry(async (token) => {
1488
+ const ref = consumeQuoteRef();
1489
+ if (event.type === "c2c") {
1490
+ return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
1491
+ }
1492
+ else if (event.type === "group" && event.groupOpenid) {
1493
+ return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
1494
+ }
1495
+ else if (event.channelId) {
1496
+ return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
1497
+ }
1498
+ });
1499
+ log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${item.content.length} chars): ${chunk.slice(0, 50)}...`);
1500
+ }
1501
+ catch (err) {
1502
+ log?.error(`[qqbot:${account.accountId}] Failed to send text chunk: ${err}`);
1503
+ }
1499
1504
  }
1500
1505
  }
1501
1506
  else if (item.type === "image") {
@@ -2049,23 +2054,26 @@ export async function startGateway(ctx) {
2049
2054
  }
2050
2055
  // 🔹 第三步:发送带公网图片的 markdown 消息
2051
2056
  if (textWithoutImages.trim()) {
2052
- try {
2053
- await sendWithTokenRetry(async (token) => {
2054
- const ref = consumeQuoteRef();
2055
- if (event.type === "c2c") {
2056
- return await sendC2CMessage(token, event.senderId, textWithoutImages, event.messageId, ref);
2057
- }
2058
- else if (event.type === "group" && event.groupOpenid) {
2059
- return await sendGroupMessage(token, event.groupOpenid, textWithoutImages, event.messageId);
2060
- }
2061
- else if (event.channelId) {
2062
- return await sendChannelMessage(token, event.channelId, textWithoutImages, event.messageId);
2063
- }
2064
- });
2065
- log?.info(`[qqbot:${account.accountId}] Sent markdown message with ${httpImageUrls.length} HTTP images (${event.type})`);
2066
- }
2067
- catch (err) {
2068
- log?.error(`[qqbot:${account.accountId}] Failed to send markdown message: ${err}`);
2057
+ const mdChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
2058
+ for (const chunk of mdChunks) {
2059
+ try {
2060
+ await sendWithTokenRetry(async (token) => {
2061
+ const ref = consumeQuoteRef();
2062
+ if (event.type === "c2c") {
2063
+ return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
2064
+ }
2065
+ else if (event.type === "group" && event.groupOpenid) {
2066
+ return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
2067
+ }
2068
+ else if (event.channelId) {
2069
+ return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
2070
+ }
2071
+ });
2072
+ log?.info(`[qqbot:${account.accountId}] Sent markdown chunk (${chunk.length}/${textWithoutImages.length} chars) with ${httpImageUrls.length} HTTP images (${event.type})`);
2073
+ }
2074
+ catch (err) {
2075
+ log?.error(`[qqbot:${account.accountId}] Failed to send markdown message chunk: ${err}`);
2076
+ }
2069
2077
  }
2070
2078
  }
2071
2079
  }
@@ -2105,21 +2113,24 @@ export async function startGateway(ctx) {
2105
2113
  log?.error(`[qqbot:${account.accountId}] Failed to send image: ${imgErr}`);
2106
2114
  }
2107
2115
  }
2108
- // 发送文本消息
2116
+ // 发送文本消息(分块)
2109
2117
  if (textWithoutImages.trim()) {
2110
- await sendWithTokenRetry(async (token) => {
2111
- const ref = consumeQuoteRef();
2112
- if (event.type === "c2c") {
2113
- return await sendC2CMessage(token, event.senderId, textWithoutImages, event.messageId, ref);
2114
- }
2115
- else if (event.type === "group" && event.groupOpenid) {
2116
- return await sendGroupMessage(token, event.groupOpenid, textWithoutImages, event.messageId);
2117
- }
2118
- else if (event.channelId) {
2119
- return await sendChannelMessage(token, event.channelId, textWithoutImages, event.messageId);
2120
- }
2121
- });
2122
- log?.info(`[qqbot:${account.accountId}] Sent text reply (${event.type})`);
2118
+ const plainChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
2119
+ for (const chunk of plainChunks) {
2120
+ await sendWithTokenRetry(async (token) => {
2121
+ const ref = consumeQuoteRef();
2122
+ if (event.type === "c2c") {
2123
+ return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
2124
+ }
2125
+ else if (event.type === "group" && event.groupOpenid) {
2126
+ return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
2127
+ }
2128
+ else if (event.channelId) {
2129
+ return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
2130
+ }
2131
+ });
2132
+ log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${textWithoutImages.length} chars) (${event.type})`);
2133
+ }
2123
2134
  }
2124
2135
  }
2125
2136
  catch (err) {
@@ -2204,7 +2215,7 @@ export async function startGateway(ctx) {
2204
2215
  },
2205
2216
  },
2206
2217
  replyOptions: {
2207
- disableBlockStreaming: false,
2218
+ disableBlockStreaming: true,
2208
2219
  },
2209
2220
  });
2210
2221
  // 等待分发完成或超时
@@ -2339,6 +2350,7 @@ export async function startGateway(ctx) {
2339
2350
  }
2340
2351
  else if (t === "RESUMED") {
2341
2352
  log?.info(`[qqbot:${account.accountId}] Session resumed`);
2353
+ onReady?.(d); // 通知框架连接已恢复,避免 health-monitor 误判 disconnected
2342
2354
  // RESUMED 也属于首次启动(gateway restart 通常走 resume)
2343
2355
  if (isFirstReadyGlobal) {
2344
2356
  isFirstReadyGlobal = false;
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.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/channel.ts CHANGED
@@ -13,38 +13,17 @@ import { startGateway } from "./gateway.js";
13
13
  import { qqbotOnboardingAdapter } from "./onboarding.js";
14
14
  import { getQQBotRuntime } from "./runtime.js";
15
15
 
16
+ /** QQ Bot 单条消息文本长度上限 */
17
+ export const TEXT_CHUNK_LIMIT = 5000;
18
+
16
19
  /**
17
- * 简单的文本分块函数
18
- * 用于预先分块长文本
20
+ * Markdown 感知的文本分块函数
21
+ * 委托给 SDK 内置的 channel.text.chunkMarkdownText
22
+ * 支持代码块自动关闭/重开、括号感知等
19
23
  */
20
- function chunkText(text: string, limit: number): string[] {
21
- if (text.length <= limit) return [text];
22
-
23
- const chunks: string[] = [];
24
- let remaining = text;
25
-
26
- while (remaining.length > 0) {
27
- if (remaining.length <= limit) {
28
- chunks.push(remaining);
29
- break;
30
- }
31
-
32
- // 尝试在换行处分割
33
- let splitAt = remaining.lastIndexOf("\n", limit);
34
- if (splitAt <= 0 || splitAt < limit * 0.5) {
35
- // 没找到合适的换行,尝试在空格处分割
36
- splitAt = remaining.lastIndexOf(" ", limit);
37
- }
38
- if (splitAt <= 0 || splitAt < limit * 0.5) {
39
- // 还是没找到,强制在 limit 处分割
40
- splitAt = limit;
41
- }
42
-
43
- chunks.push(remaining.slice(0, splitAt));
44
- remaining = remaining.slice(splitAt).trimStart();
45
- }
46
-
47
- return chunks;
24
+ export function chunkText(text: string, limit: number): string[] {
25
+ const runtime = getQQBotRuntime();
26
+ return runtime.channel.text.chunkMarkdownText(text, limit);
48
27
  }
49
28
 
50
29
  export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
@@ -230,9 +209,9 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
230
209
  },
231
210
  outbound: {
232
211
  deliveryMode: "direct",
233
- chunker: chunkText,
212
+ chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
234
213
  chunkerMode: "markdown",
235
- textChunkLimit: 20000,
214
+ textChunkLimit: 5000,
236
215
  sendText: async ({ to, text, accountId, replyToId, cfg }) => {
237
216
  console.log(`[qqbot:channel] sendText called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, text.length=${text?.length ?? 0}`);
238
217
  console.log(`[qqbot:channel] sendText text preview: ${text?.slice(0, 100)}${(text?.length ?? 0) > 100 ? "..." : ""}`);
package/src/gateway.ts CHANGED
@@ -18,6 +18,7 @@ import { checkFileSize, readFileAsync, fileExistsAsync, isLargeFile, formatFileS
18
18
  import { getQQBotDataDir, isLocalPath as isLocalFilePath, normalizePath, sanitizeFileName, runDiagnostics } from "./utils/platform.js";
19
19
  import { MSG, formatMediaErrorMessage } from "./user-messages.js";
20
20
  import { sendPhoto, sendVoice, sendVideoMsg, sendDocument, sendMedia as sendMediaAuto, type MediaTargetContext } from "./outbound.js";
21
+ import { chunkText, TEXT_CHUNK_LIMIT } from "./channel.js";
21
22
 
22
23
  /**
23
24
  * 通用 OpenAI 兼容 STT(语音转文字)
@@ -1659,20 +1660,24 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
1659
1660
 
1660
1661
  for (const item of sendQueue) {
1661
1662
  if (item.type === "text") {
1662
- try {
1663
- await sendWithTokenRetry(async (token) => {
1664
- const ref = consumeQuoteRef();
1665
- if (event.type === "c2c") {
1666
- return await sendC2CMessage(token, event.senderId, item.content, event.messageId, ref);
1667
- } else if (event.type === "group" && event.groupOpenid) {
1668
- return await sendGroupMessage(token, event.groupOpenid, item.content, event.messageId);
1669
- } else if (event.channelId) {
1670
- return await sendChannelMessage(token, event.channelId, item.content, event.messageId);
1671
- }
1672
- });
1673
- log?.info(`[qqbot:${account.accountId}] Sent text: ${item.content.slice(0, 50)}...`);
1674
- } catch (err) {
1675
- log?.error(`[qqbot:${account.accountId}] Failed to send text: ${err}`);
1663
+ // 对长文本进行分块发送
1664
+ const textChunks = getQQBotRuntime().channel.text.chunkMarkdownText(item.content, TEXT_CHUNK_LIMIT);
1665
+ for (const chunk of textChunks) {
1666
+ try {
1667
+ await sendWithTokenRetry(async (token) => {
1668
+ const ref = consumeQuoteRef();
1669
+ if (event.type === "c2c") {
1670
+ return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
1671
+ } else if (event.type === "group" && event.groupOpenid) {
1672
+ return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
1673
+ } else if (event.channelId) {
1674
+ return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
1675
+ }
1676
+ });
1677
+ log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${item.content.length} chars): ${chunk.slice(0, 50)}...`);
1678
+ } catch (err) {
1679
+ log?.error(`[qqbot:${account.accountId}] Failed to send text chunk: ${err}`);
1680
+ }
1676
1681
  }
1677
1682
  } else if (item.type === "image") {
1678
1683
  const result = await sendPhoto(mediaTarget, item.content);
@@ -2218,20 +2223,23 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2218
2223
 
2219
2224
  // 🔹 第三步:发送带公网图片的 markdown 消息
2220
2225
  if (textWithoutImages.trim()) {
2221
- try {
2222
- await sendWithTokenRetry(async (token) => {
2223
- const ref = consumeQuoteRef();
2224
- if (event.type === "c2c") {
2225
- return await sendC2CMessage(token, event.senderId, textWithoutImages, event.messageId, ref);
2226
- } else if (event.type === "group" && event.groupOpenid) {
2227
- return await sendGroupMessage(token, event.groupOpenid, textWithoutImages, event.messageId);
2228
- } else if (event.channelId) {
2229
- return await sendChannelMessage(token, event.channelId, textWithoutImages, event.messageId);
2230
- }
2231
- });
2232
- log?.info(`[qqbot:${account.accountId}] Sent markdown message with ${httpImageUrls.length} HTTP images (${event.type})`);
2233
- } catch (err) {
2234
- log?.error(`[qqbot:${account.accountId}] Failed to send markdown message: ${err}`);
2226
+ const mdChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
2227
+ for (const chunk of mdChunks) {
2228
+ try {
2229
+ await sendWithTokenRetry(async (token) => {
2230
+ const ref = consumeQuoteRef();
2231
+ if (event.type === "c2c") {
2232
+ return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
2233
+ } else if (event.type === "group" && event.groupOpenid) {
2234
+ return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
2235
+ } else if (event.channelId) {
2236
+ return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
2237
+ }
2238
+ });
2239
+ log?.info(`[qqbot:${account.accountId}] Sent markdown chunk (${chunk.length}/${textWithoutImages.length} chars) with ${httpImageUrls.length} HTTP images (${event.type})`);
2240
+ } catch (err) {
2241
+ log?.error(`[qqbot:${account.accountId}] Failed to send markdown message chunk: ${err}`);
2242
+ }
2235
2243
  }
2236
2244
  }
2237
2245
  } else {
@@ -2271,19 +2279,22 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2271
2279
  }
2272
2280
  }
2273
2281
 
2274
- // 发送文本消息
2282
+ // 发送文本消息(分块)
2275
2283
  if (textWithoutImages.trim()) {
2276
- await sendWithTokenRetry(async (token) => {
2277
- const ref = consumeQuoteRef();
2278
- if (event.type === "c2c") {
2279
- return await sendC2CMessage(token, event.senderId, textWithoutImages, event.messageId, ref);
2280
- } else if (event.type === "group" && event.groupOpenid) {
2281
- return await sendGroupMessage(token, event.groupOpenid, textWithoutImages, event.messageId);
2282
- } else if (event.channelId) {
2283
- return await sendChannelMessage(token, event.channelId, textWithoutImages, event.messageId);
2284
- }
2285
- });
2286
- log?.info(`[qqbot:${account.accountId}] Sent text reply (${event.type})`);
2284
+ const plainChunks = chunkText(textWithoutImages, TEXT_CHUNK_LIMIT);
2285
+ for (const chunk of plainChunks) {
2286
+ await sendWithTokenRetry(async (token) => {
2287
+ const ref = consumeQuoteRef();
2288
+ if (event.type === "c2c") {
2289
+ return await sendC2CMessage(token, event.senderId, chunk, event.messageId, ref);
2290
+ } else if (event.type === "group" && event.groupOpenid) {
2291
+ return await sendGroupMessage(token, event.groupOpenid, chunk, event.messageId);
2292
+ } else if (event.channelId) {
2293
+ return await sendChannelMessage(token, event.channelId, chunk, event.messageId);
2294
+ }
2295
+ });
2296
+ log?.info(`[qqbot:${account.accountId}] Sent text chunk (${chunk.length}/${textWithoutImages.length} chars) (${event.type})`);
2297
+ }
2287
2298
  }
2288
2299
  } catch (err) {
2289
2300
  log?.error(`[qqbot:${account.accountId}] Send failed: ${err}`);
@@ -2366,7 +2377,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2366
2377
  },
2367
2378
  },
2368
2379
  replyOptions: {
2369
- disableBlockStreaming: false,
2380
+ disableBlockStreaming: true,
2370
2381
  },
2371
2382
  });
2372
2383
 
@@ -2504,6 +2515,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
2504
2515
  } // end isFirstReady
2505
2516
  } else if (t === "RESUMED") {
2506
2517
  log?.info(`[qqbot:${account.accountId}] Session resumed`);
2518
+ onReady?.(d); // 通知框架连接已恢复,避免 health-monitor 误判 disconnected
2507
2519
  // RESUMED 也属于首次启动(gateway restart 通常走 resume)
2508
2520
  if (isFirstReadyGlobal) {
2509
2521
  isFirstReadyGlobal = false;