@jerryliang122/openclaw-qqbot 1.0.3 → 1.0.5

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.cts CHANGED
@@ -860,8 +860,13 @@ declare class StreamingController {
860
860
  private session;
861
861
  /** QQ 已接受的最新文本 — 单源真理 */
862
862
  private lastAcceptedFull;
863
- /** 已成功发送的分片数(降级:=0 则走静态消息兜底) */
863
+ /** 已成功发送的分片数(降级:=0 且无外部投递则走静态消息兜底) */
864
864
  private sentChunkCount;
865
+ /**
866
+ * 标记:内容已通过非流式通道成功投递(如 ask_user 按钮卡片)。
867
+ * 置 true 后 handleFinalize 不再走 fallback 路径,避免框架误报 outcome=error。
868
+ */
869
+ private _deliveredExternally;
865
870
  /** 同步标志:收到第一个 onPartialReply 即置 true(不等 async 完成) */
866
871
  private _hasStarted;
867
872
  /** 串行队列 */
@@ -882,6 +887,11 @@ declare class StreamingController {
882
887
  get hasStarted(): boolean;
883
888
  get isTerminal(): boolean;
884
889
  get shouldFallbackToStatic(): boolean;
890
+ /**
891
+ * 告知控制器内容已通过外部通道投递(如 ask_user 卡片)。
892
+ * 后续 finalize() 不会走 fallback 路径,框架不会误报 outcome=error。
893
+ */
894
+ markDeliveredExternally(): void;
885
895
  onPartialReply(text: string): Promise<void>;
886
896
  finalize(): Promise<void>;
887
897
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jerryliang122/openclaw-qqbot",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "QQ Bot channel plugin for OpenClaw (independently maintained fork)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -298,6 +298,8 @@ export async function dispatchToOpenClaw(
298
298
  try {
299
299
  await bot.sendTextWithKeyboard(replyTarget, text, keyboard as never);
300
300
  outboundSendOk++;
301
+ // 标记流式控制器:内容已通过外部通道投递,finalize 不走 fallback
302
+ streamingController?.markDeliveredExternally();
301
303
  dlog?.debug(`[question] sent ask_user with keyboard questionId=${questionId} options=${optionValues.length}`);
302
304
  return;
303
305
  } catch (err) {
@@ -331,6 +333,8 @@ export async function dispatchToOpenClaw(
331
333
  await bot.sendTextWithKeyboard(replyTarget, cardText, keyboard as never);
332
334
  outboundSendOk++;
333
335
  }
336
+ // 标记流式控制器:内容已通过外部通道投递,finalize 不走 fallback
337
+ streamingController?.markDeliveredExternally();
334
338
  dlog?.debug(`[question] sent multi-question ask_user questionId=${questionId} questions=${questions.length}`);
335
339
  return;
336
340
  } catch (err) {
@@ -356,6 +360,10 @@ export async function dispatchToOpenClaw(
356
360
  }
357
361
 
358
362
  // ── 2. 流式路径:流式已启动且未降级 -> 跳过静态发送 ──
363
+ // 例外:isError payload(agent run 失败通知)必须落到默认路径发出。
364
+ // 失败文案从未进入流式通道(模型死在半路),「final 已由流式发过」的
365
+ // 去重假设不成立——早退 return 会把通知吞掉,用户只见首分片后静默
366
+ // (issue #8:静态流式首分片 1062 字符已发,模型超时后无任何提示)。
359
367
  if (streamingController?.hasStarted && !streamingController?.shouldFallbackToStatic) {
360
368
  if (streamingController.isStaticSendMode) {
361
369
  // static 模式:flush 主路径由 onToolStart 驱动(工具开始前,绕开 SDK
@@ -370,8 +378,13 @@ export async function dispatchToOpenClaw(
370
378
  // stream 模式:tool/final 时收尾当前打字机流(原行为)
371
379
  await streamingController.finalize();
372
380
  }
373
- if (!streamingController.shouldFallbackToStatic) return;
374
- dlog?.warn(`streaming fallback to static`);
381
+ if (payload.isError === true) {
382
+ dlog?.info(`run-failure notice bypasses streaming dedup (kind=${kind ?? 'none'} textLen=${text.length})`);
383
+ } else if (!streamingController.shouldFallbackToStatic) {
384
+ return;
385
+ } else {
386
+ dlog?.warn(`streaming fallback to static`);
387
+ }
375
388
  }
376
389
 
377
390
  // ── 3. 文本去重:同文本已发过 -> 跳过 ──
@@ -27,6 +27,11 @@ export interface DeliverPayload {
27
27
  mediaUrl?: string;
28
28
  mediaUrls?: string[];
29
29
  audioAsVoice?: boolean;
30
+ /**
31
+ * agent run 失败通知 payload(框架 markAgentRunFailureReplyPayload 必置 true)。
32
+ * 这类文本从未进入流式通道(模型死在半路),流式去重早退对它不适用。
33
+ */
34
+ isError?: boolean;
30
35
  }
31
36
 
32
37
  export interface DeliverInfo {
@@ -51,9 +51,15 @@ export class StreamingController {
51
51
  /** QQ 已接受的最新文本 — 单源真理 */
52
52
  private lastAcceptedFull = '';
53
53
 
54
- /** 已成功发送的分片数(降级:=0 则走静态消息兜底) */
54
+ /** 已成功发送的分片数(降级:=0 且无外部投递则走静态消息兜底) */
55
55
  private sentChunkCount = 0;
56
56
 
57
+ /**
58
+ * 标记:内容已通过非流式通道成功投递(如 ask_user 按钮卡片)。
59
+ * 置 true 后 handleFinalize 不再走 fallback 路径,避免框架误报 outcome=error。
60
+ */
61
+ private _deliveredExternally = false;
62
+
57
63
  /** 同步标志:收到第一个 onPartialReply 即置 true(不等 async 完成) */
58
64
  private _hasStarted = false;
59
65
 
@@ -87,7 +93,15 @@ export class StreamingController {
87
93
  }
88
94
 
89
95
  get shouldFallbackToStatic(): boolean {
90
- return this.isTerminal && this.sentChunkCount === 0;
96
+ return this.isTerminal && this.sentChunkCount === 0 && !this._deliveredExternally;
97
+ }
98
+
99
+ /**
100
+ * 告知控制器内容已通过外部通道投递(如 ask_user 卡片)。
101
+ * 后续 finalize() 不会走 fallback 路径,框架不会误报 outcome=error。
102
+ */
103
+ markDeliveredExternally(): void {
104
+ this._deliveredExternally = true;
91
105
  }
92
106
 
93
107
  // ── 入口 ──
@@ -212,6 +226,9 @@ export class StreamingController {
212
226
  } else if (this.sentChunkCount > 0 && this.lastAcceptedFull) {
213
227
  // 未提供 sendStatic → 降级标记,由上层兜底发送
214
228
  this.transition('failed', 'finalize:no_sendstatic');
229
+ } else if (this._deliveredExternally) {
230
+ // 内容已通过外部通道投递(如 ask_user 卡片),非失败
231
+ this.transition('done', 'finalize');
215
232
  } else {
216
233
  this.transition('done', 'finalize:fallback');
217
234
  }
@@ -227,8 +244,8 @@ export class StreamingController {
227
244
  }
228
245
 
229
246
  // 无会话 — 视有无下发决定终态
230
- if (this.sentChunkCount > 0) {
231
- this.transition('done', 'finalize:no_session');
247
+ if (this.sentChunkCount > 0 || this._deliveredExternally) {
248
+ this.transition('done', this.sentChunkCount > 0 ? 'finalize:no_session' : 'finalize');
232
249
  } else {
233
250
  this.transition('done', 'finalize:fallback');
234
251
  }