@openclaw-china/qqbot 2026.3.16 → 2026.3.18

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/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ declare const QQBotC2CMarkdownDeliveryModeSchema: z.ZodDefault<z.ZodOptional<z.Z
4
4
  type QQBotC2CMarkdownDeliveryMode = z.input<typeof QQBotC2CMarkdownDeliveryModeSchema>;
5
5
  declare const QQBotC2CMarkdownChunkStrategySchema: z.ZodDefault<z.ZodOptional<z.ZodEnum<["markdown-block", "length"]>>>;
6
6
  type QQBotC2CMarkdownChunkStrategy = z.input<typeof QQBotC2CMarkdownChunkStrategySchema>;
7
+ declare const QQBotTypingHeartbeatModeSchema: z.ZodDefault<z.ZodOptional<z.ZodEnum<["none", "idle", "always"]>>>;
8
+ type QQBotTypingHeartbeatMode = z.input<typeof QQBotTypingHeartbeatModeSchema>;
7
9
  declare const QQBotAccountSchema: z.ZodObject<{
8
10
  name: z.ZodOptional<z.ZodString>;
9
11
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -29,6 +31,9 @@ declare const QQBotAccountSchema: z.ZodObject<{
29
31
  markdownSupport: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
30
32
  c2cMarkdownDeliveryMode: z.ZodDefault<z.ZodOptional<z.ZodEnum<["passive", "proactive-table-only", "proactive-all"]>>>;
31
33
  c2cMarkdownChunkStrategy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["markdown-block", "length"]>>>;
34
+ typingHeartbeatMode: z.ZodDefault<z.ZodOptional<z.ZodEnum<["none", "idle", "always"]>>>;
35
+ typingHeartbeatIntervalMs: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
36
+ typingInputSeconds: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
37
  dmPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "pairing", "allowlist"]>>>;
33
38
  groupPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "allowlist", "disabled"]>>>;
34
39
  requireMention: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -55,6 +60,9 @@ declare const QQBotAccountSchema: z.ZodObject<{
55
60
  markdownSupport: boolean;
56
61
  c2cMarkdownDeliveryMode: "passive" | "proactive-table-only" | "proactive-all";
57
62
  c2cMarkdownChunkStrategy: "markdown-block" | "length";
63
+ typingHeartbeatMode: "none" | "idle" | "always";
64
+ typingHeartbeatIntervalMs: number;
65
+ typingInputSeconds: number;
58
66
  dmPolicy: "open" | "pairing" | "allowlist";
59
67
  groupPolicy: "open" | "allowlist" | "disabled";
60
68
  requireMention: boolean;
@@ -97,6 +105,9 @@ declare const QQBotAccountSchema: z.ZodObject<{
97
105
  markdownSupport?: boolean | undefined;
98
106
  c2cMarkdownDeliveryMode?: "passive" | "proactive-table-only" | "proactive-all" | undefined;
99
107
  c2cMarkdownChunkStrategy?: "markdown-block" | "length" | undefined;
108
+ typingHeartbeatMode?: "none" | "idle" | "always" | undefined;
109
+ typingHeartbeatIntervalMs?: number | undefined;
110
+ typingInputSeconds?: number | undefined;
100
111
  dmPolicy?: "open" | "pairing" | "allowlist" | undefined;
101
112
  groupPolicy?: "open" | "allowlist" | "disabled" | undefined;
102
113
  requireMention?: boolean | undefined;
@@ -139,6 +150,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
139
150
  markdownSupport: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
140
151
  c2cMarkdownDeliveryMode: z.ZodDefault<z.ZodOptional<z.ZodEnum<["passive", "proactive-table-only", "proactive-all"]>>>;
141
152
  c2cMarkdownChunkStrategy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["markdown-block", "length"]>>>;
153
+ typingHeartbeatMode: z.ZodDefault<z.ZodOptional<z.ZodEnum<["none", "idle", "always"]>>>;
154
+ typingHeartbeatIntervalMs: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
155
+ typingInputSeconds: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
142
156
  dmPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "pairing", "allowlist"]>>>;
143
157
  groupPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "allowlist", "disabled"]>>>;
144
158
  requireMention: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -188,6 +202,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
188
202
  markdownSupport: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
189
203
  c2cMarkdownDeliveryMode: z.ZodDefault<z.ZodOptional<z.ZodEnum<["passive", "proactive-table-only", "proactive-all"]>>>;
190
204
  c2cMarkdownChunkStrategy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["markdown-block", "length"]>>>;
205
+ typingHeartbeatMode: z.ZodDefault<z.ZodOptional<z.ZodEnum<["none", "idle", "always"]>>>;
206
+ typingHeartbeatIntervalMs: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
207
+ typingInputSeconds: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
191
208
  dmPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "pairing", "allowlist"]>>>;
192
209
  groupPolicy: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "allowlist", "disabled"]>>>;
193
210
  requireMention: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
@@ -214,6 +231,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
214
231
  markdownSupport: boolean;
215
232
  c2cMarkdownDeliveryMode: "passive" | "proactive-table-only" | "proactive-all";
216
233
  c2cMarkdownChunkStrategy: "markdown-block" | "length";
234
+ typingHeartbeatMode: "none" | "idle" | "always";
235
+ typingHeartbeatIntervalMs: number;
236
+ typingInputSeconds: number;
217
237
  dmPolicy: "open" | "pairing" | "allowlist";
218
238
  groupPolicy: "open" | "allowlist" | "disabled";
219
239
  requireMention: boolean;
@@ -256,6 +276,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
256
276
  markdownSupport?: boolean | undefined;
257
277
  c2cMarkdownDeliveryMode?: "passive" | "proactive-table-only" | "proactive-all" | undefined;
258
278
  c2cMarkdownChunkStrategy?: "markdown-block" | "length" | undefined;
279
+ typingHeartbeatMode?: "none" | "idle" | "always" | undefined;
280
+ typingHeartbeatIntervalMs?: number | undefined;
281
+ typingInputSeconds?: number | undefined;
259
282
  dmPolicy?: "open" | "pairing" | "allowlist" | undefined;
260
283
  groupPolicy?: "open" | "allowlist" | "disabled" | undefined;
261
284
  requireMention?: boolean | undefined;
@@ -277,6 +300,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
277
300
  markdownSupport: boolean;
278
301
  c2cMarkdownDeliveryMode: "passive" | "proactive-table-only" | "proactive-all";
279
302
  c2cMarkdownChunkStrategy: "markdown-block" | "length";
303
+ typingHeartbeatMode: "none" | "idle" | "always";
304
+ typingHeartbeatIntervalMs: number;
305
+ typingInputSeconds: number;
280
306
  dmPolicy: "open" | "pairing" | "allowlist";
281
307
  groupPolicy: "open" | "allowlist" | "disabled";
282
308
  requireMention: boolean;
@@ -309,6 +335,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
309
335
  markdownSupport: boolean;
310
336
  c2cMarkdownDeliveryMode: "passive" | "proactive-table-only" | "proactive-all";
311
337
  c2cMarkdownChunkStrategy: "markdown-block" | "length";
338
+ typingHeartbeatMode: "none" | "idle" | "always";
339
+ typingHeartbeatIntervalMs: number;
340
+ typingInputSeconds: number;
312
341
  dmPolicy: "open" | "pairing" | "allowlist";
313
342
  groupPolicy: "open" | "allowlist" | "disabled";
314
343
  requireMention: boolean;
@@ -352,6 +381,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
352
381
  markdownSupport?: boolean | undefined;
353
382
  c2cMarkdownDeliveryMode?: "passive" | "proactive-table-only" | "proactive-all" | undefined;
354
383
  c2cMarkdownChunkStrategy?: "markdown-block" | "length" | undefined;
384
+ typingHeartbeatMode?: "none" | "idle" | "always" | undefined;
385
+ typingHeartbeatIntervalMs?: number | undefined;
386
+ typingInputSeconds?: number | undefined;
355
387
  dmPolicy?: "open" | "pairing" | "allowlist" | undefined;
356
388
  groupPolicy?: "open" | "allowlist" | "disabled" | undefined;
357
389
  requireMention?: boolean | undefined;
@@ -384,6 +416,9 @@ declare const QQBotConfigSchema: z.ZodObject<{
384
416
  markdownSupport?: boolean | undefined;
385
417
  c2cMarkdownDeliveryMode?: "passive" | "proactive-table-only" | "proactive-all" | undefined;
386
418
  c2cMarkdownChunkStrategy?: "markdown-block" | "length" | undefined;
419
+ typingHeartbeatMode?: "none" | "idle" | "always" | undefined;
420
+ typingHeartbeatIntervalMs?: number | undefined;
421
+ typingInputSeconds?: number | undefined;
387
422
  dmPolicy?: "open" | "pairing" | "allowlist" | undefined;
388
423
  groupPolicy?: "open" | "allowlist" | "disabled" | undefined;
389
424
  requireMention?: boolean | undefined;
@@ -419,6 +454,7 @@ interface ResolvedQQBotAccount {
419
454
  markdownSupport?: boolean;
420
455
  c2cMarkdownDeliveryMode?: QQBotC2CMarkdownDeliveryMode;
421
456
  c2cMarkdownChunkStrategy?: QQBotC2CMarkdownChunkStrategy;
457
+ typingHeartbeatMode?: QQBotTypingHeartbeatMode;
422
458
  }
423
459
  interface QQBotSendResult {
424
460
  channel: "qqbot";
package/dist/index.js CHANGED
@@ -4228,6 +4228,10 @@ var displayAliasesSchema = external_exports.record(
4228
4228
  ).optional();
4229
4229
  var QQBotC2CMarkdownDeliveryModeSchema = external_exports.enum(["passive", "proactive-table-only", "proactive-all"]).optional().default("proactive-table-only");
4230
4230
  var QQBotC2CMarkdownChunkStrategySchema = external_exports.enum(["markdown-block", "length"]).optional().default("markdown-block");
4231
+ var QQBotTypingHeartbeatModeSchema = external_exports.enum(["none", "idle", "always"]).optional().default("idle");
4232
+ var DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE = "idle";
4233
+ var DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS = 5e3;
4234
+ var DEFAULT_QQBOT_TYPING_INPUT_SECONDS = 60;
4231
4235
  var QQBotAccountSchema = external_exports.object({
4232
4236
  name: external_exports.string().optional(),
4233
4237
  enabled: external_exports.boolean().optional(),
@@ -4243,6 +4247,13 @@ var QQBotAccountSchema = external_exports.object({
4243
4247
  markdownSupport: external_exports.boolean().optional().default(true),
4244
4248
  c2cMarkdownDeliveryMode: QQBotC2CMarkdownDeliveryModeSchema,
4245
4249
  c2cMarkdownChunkStrategy: QQBotC2CMarkdownChunkStrategySchema,
4250
+ typingHeartbeatMode: QQBotTypingHeartbeatModeSchema,
4251
+ typingHeartbeatIntervalMs: external_exports.number().int().positive().optional().default(
4252
+ DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS
4253
+ ),
4254
+ typingInputSeconds: external_exports.number().int().positive().optional().default(
4255
+ DEFAULT_QQBOT_TYPING_INPUT_SECONDS
4256
+ ),
4246
4257
  dmPolicy: external_exports.enum(["open", "pairing", "allowlist"]).optional().default("open"),
4247
4258
  groupPolicy: external_exports.enum(["open", "allowlist", "disabled"]).optional().default("open"),
4248
4259
  requireMention: external_exports.boolean().optional().default(true),
@@ -4292,6 +4303,15 @@ function resolveInboundMediaKeepDays(config) {
4292
4303
  function resolveQQBotAutoSendLocalPathMedia(config) {
4293
4304
  return config?.autoSendLocalPathMedia ?? true;
4294
4305
  }
4306
+ function resolveQQBotTypingHeartbeatMode(config) {
4307
+ return config?.typingHeartbeatMode ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_MODE;
4308
+ }
4309
+ function resolveQQBotTypingHeartbeatIntervalMs(config) {
4310
+ return config?.typingHeartbeatIntervalMs ?? DEFAULT_QQBOT_TYPING_HEARTBEAT_INTERVAL_MS;
4311
+ }
4312
+ function resolveQQBotTypingInputSeconds(config) {
4313
+ return config?.typingInputSeconds ?? DEFAULT_QQBOT_TYPING_INPUT_SECONDS;
4314
+ }
4295
4315
  function resolveInboundMediaTempDir() {
4296
4316
  return DEFAULT_INBOUND_MEDIA_TEMP_DIR;
4297
4317
  }
@@ -9623,6 +9643,37 @@ function startLongTaskNoticeTimer(params) {
9623
9643
  }
9624
9644
  };
9625
9645
  }
9646
+ function startQQBotTypingHeartbeat(params) {
9647
+ const { intervalMs, renew, shouldRenew } = params;
9648
+ let stopped = false;
9649
+ let timer = null;
9650
+ let renewalInFlight = false;
9651
+ const clear = () => {
9652
+ if (!timer) return;
9653
+ clearInterval(timer);
9654
+ timer = null;
9655
+ };
9656
+ const stop = () => {
9657
+ if (stopped) return;
9658
+ stopped = true;
9659
+ clear();
9660
+ };
9661
+ if (intervalMs > 0) {
9662
+ timer = setInterval(() => {
9663
+ if (stopped || renewalInFlight) return;
9664
+ if (shouldRenew && !shouldRenew()) return;
9665
+ renewalInFlight = true;
9666
+ void renew().catch(() => void 0).finally(() => {
9667
+ renewalInFlight = false;
9668
+ });
9669
+ }, intervalMs);
9670
+ timer.unref?.();
9671
+ }
9672
+ return {
9673
+ stop,
9674
+ dispose: stop
9675
+ };
9676
+ }
9626
9677
  function isHttpUrl3(value) {
9627
9678
  return /^https?:\/\//i.test(value);
9628
9679
  }
@@ -11029,6 +11080,9 @@ async function dispatchToAgent(params) {
11029
11080
  };
11030
11081
  const target = resolveChatTarget(inbound);
11031
11082
  const outboundAccountId = route.accountId ?? accountId;
11083
+ const typingHeartbeatMode = resolveQQBotTypingHeartbeatMode(qqCfg);
11084
+ const typingHeartbeatIntervalMs = resolveQQBotTypingHeartbeatIntervalMs(qqCfg);
11085
+ const typingInputSeconds = resolveQQBotTypingInputSeconds(qqCfg);
11032
11086
  let typingRefIdx;
11033
11087
  if (inbound.c2cOpenid && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
11034
11088
  const typing = await qqbotOutbound.sendTyping({
@@ -11036,7 +11090,7 @@ async function dispatchToAgent(params) {
11036
11090
  to: `user:${inbound.c2cOpenid}`,
11037
11091
  replyToId: inbound.messageId,
11038
11092
  replyEventId: inbound.eventId,
11039
- inputSecond: 60,
11093
+ inputSecond: typingInputSeconds,
11040
11094
  accountId: outboundAccountId
11041
11095
  });
11042
11096
  if (typing.error) {
@@ -11052,10 +11106,15 @@ async function dispatchToAgent(params) {
11052
11106
  }
11053
11107
  let replyDelivered = false;
11054
11108
  let groupMessageInterfaceBlocked = false;
11109
+ let lastVisibleOutboundAt = Date.now();
11110
+ let typingHeartbeat = null;
11055
11111
  const markReplyDelivered = () => {
11056
11112
  replyDelivered = true;
11057
11113
  longTaskNotice.markReplyDelivered();
11058
11114
  };
11115
+ const markVisibleOutboundStarted = () => {
11116
+ lastVisibleOutboundAt = Date.now();
11117
+ };
11059
11118
  const markGroupMessageInterfaceBlocked = (error) => {
11060
11119
  if (!isQQBotGroupMessageInterfaceBlocked(error)) return;
11061
11120
  if (!groupMessageInterfaceBlocked) {
@@ -11063,11 +11122,40 @@ async function dispatchToAgent(params) {
11063
11122
  }
11064
11123
  groupMessageInterfaceBlocked = true;
11065
11124
  };
11125
+ if (inbound.c2cOpenid && typingHeartbeatMode !== "none" && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
11126
+ typingHeartbeat = startQQBotTypingHeartbeat({
11127
+ intervalMs: typingHeartbeatIntervalMs,
11128
+ shouldRenew: () => {
11129
+ if (shouldSuppressVisibleReplies()) {
11130
+ return false;
11131
+ }
11132
+ if (typingHeartbeatMode === "always") {
11133
+ return true;
11134
+ }
11135
+ return Date.now() - lastVisibleOutboundAt >= typingHeartbeatIntervalMs;
11136
+ },
11137
+ renew: async () => {
11138
+ try {
11139
+ const typing = await qqbotOutbound.sendTyping({
11140
+ cfg: { channels: { qqbot: qqCfg } },
11141
+ to: `user:${inbound.c2cOpenid}`,
11142
+ replyToId: inbound.messageId,
11143
+ replyEventId: inbound.eventId,
11144
+ inputSecond: typingInputSeconds,
11145
+ accountId: outboundAccountId
11146
+ });
11147
+ void typing;
11148
+ } catch {
11149
+ }
11150
+ }
11151
+ });
11152
+ }
11066
11153
  const longTaskNotice = startLongTaskNoticeTimer({
11067
11154
  delayMs: qqCfg.longTaskNoticeDelayMs ?? DEFAULT_LONG_TASK_NOTICE_DELAY_MS,
11068
11155
  logger,
11069
11156
  sendNotice: async () => {
11070
11157
  if (groupMessageInterfaceBlocked || isFastAbortCommand || shouldSuppressVisibleReplies()) return;
11158
+ markVisibleOutboundStarted();
11071
11159
  const result = await qqbotOutbound.sendText({
11072
11160
  cfg: { channels: { qqbot: qqCfg } },
11073
11161
  to: target.to,
@@ -11080,7 +11168,7 @@ async function dispatchToAgent(params) {
11080
11168
  logger.warn(`send long-task notice failed: ${result.error}`);
11081
11169
  markGroupMessageInterfaceBlocked(result.error);
11082
11170
  } else {
11083
- replyDelivered = true;
11171
+ markReplyDelivered();
11084
11172
  }
11085
11173
  }
11086
11174
  });
@@ -11104,6 +11192,7 @@ async function dispatchToAgent(params) {
11104
11192
  if (shouldSuppressVisibleReplies()) {
11105
11193
  return;
11106
11194
  }
11195
+ markVisibleOutboundStarted();
11107
11196
  const fallback = await qqbotOutbound.sendText({
11108
11197
  cfg: { channels: { qqbot: qqCfg } },
11109
11198
  to: target.to,
@@ -11116,7 +11205,7 @@ async function dispatchToAgent(params) {
11116
11205
  logger.error(`sendText ASR fallback failed: ${fallback.error}`);
11117
11206
  markGroupMessageInterfaceBlocked(fallback.error);
11118
11207
  } else {
11119
- replyDelivered = true;
11208
+ markReplyDelivered();
11120
11209
  }
11121
11210
  return;
11122
11211
  }
@@ -11322,6 +11411,9 @@ async function dispatchToAgent(params) {
11322
11411
  `delivery=${deliveryLabel} to=${target.to} chunks=${textChunks.length} media=${mediaQueue.length} replyToId=${textReplyRefs.replyToId ? "yes" : "no"} replyEventId=${textReplyRefs.replyEventId ? "yes" : "no"} phase=${params2.phase} tableMode=${String(resolvedTableMode)} chunkMode=${String(chunkMode ?? "default")} chunkStrategy=${c2cMarkdownChunkStrategy}`
11323
11412
  );
11324
11413
  if (!shouldSuppressVisibleReplies()) {
11414
+ if (mediaQueue.length > 0) {
11415
+ markVisibleOutboundStarted();
11416
+ }
11325
11417
  await sendQQBotMediaWithFallback({
11326
11418
  qqCfg,
11327
11419
  to: target.to,
@@ -11350,6 +11442,7 @@ async function dispatchToAgent(params) {
11350
11442
  logger.info(
11351
11443
  `delivery=${deliveryLabel} segment=1/1 chunk=${chunkIndex + 1}/${textChunks.length} phase=${params2.phase} preview=${formatQQBotOutboundPreview(chunk)}`
11352
11444
  );
11445
+ markVisibleOutboundStarted();
11353
11446
  const result = await qqbotOutbound.sendText({
11354
11447
  cfg: { channels: { qqbot: qqCfg } },
11355
11448
  to: target.to,
@@ -11464,6 +11557,7 @@ async function dispatchToAgent(params) {
11464
11557
  if (shouldSuppressVisibleReplies()) {
11465
11558
  return;
11466
11559
  }
11560
+ markVisibleOutboundStarted();
11467
11561
  const result = await qqbotOutbound.sendText({
11468
11562
  cfg: { channels: { qqbot: qqCfg } },
11469
11563
  to: target.to,
@@ -11483,6 +11577,9 @@ async function dispatchToAgent(params) {
11483
11577
  if (shouldSuppressVisibleReplies()) {
11484
11578
  return;
11485
11579
  }
11580
+ if (mediaQueue.length > 0) {
11581
+ markVisibleOutboundStarted();
11582
+ }
11486
11583
  await sendQQBotMediaWithFallback({
11487
11584
  qqCfg,
11488
11585
  to: target.to,
@@ -11587,6 +11684,7 @@ async function dispatchToAgent(params) {
11587
11684
  });
11588
11685
  if (noReplyFallback && !groupMessageInterfaceBlocked && !isFastAbortCommand && !shouldSuppressVisibleReplies()) {
11589
11686
  logger.info("no visible reply generated for group mention; sending fallback text");
11687
+ markVisibleOutboundStarted();
11590
11688
  const fallbackResult = await qqbotOutbound.sendText({
11591
11689
  cfg: { channels: { qqbot: qqCfg } },
11592
11690
  to: target.to,
@@ -11603,6 +11701,7 @@ async function dispatchToAgent(params) {
11603
11701
  }
11604
11702
  }
11605
11703
  } finally {
11704
+ typingHeartbeat?.dispose();
11606
11705
  longTaskNotice.dispose();
11607
11706
  try {
11608
11707
  await pruneInboundMediaDir({