From ace724c60980013597862acb51395ebe7c2adf97 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Thu, 30 Jul 2026 15:02:08 +0800 Subject: [PATCH] fix(hub): always send interrupt notice when card finalize fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If StreamingAgentCard.finish cannot patch the live card, plain-text fallback can still succeed with partial answer text. Interrupt is terminal — always emit the explicit 已中断 notice when the card path failed so teachers see the abort. Harden the integration assertion with waitFor. --- hub/src/feishu/card/streaming-card.ts | 15 +++++++++------ hub/test/integration/trigger.test.ts | 4 +++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/hub/src/feishu/card/streaming-card.ts b/hub/src/feishu/card/streaming-card.ts index f703e0c..27e4f86 100644 --- a/hub/src/feishu/card/streaming-card.ts +++ b/hub/src/feishu/card/streaming-card.ts @@ -173,17 +173,20 @@ export class StreamingAgentCard { ); } - let updated = true; + let cardUpdated = true; if (answerText.length > 0 || segments.length > 0) { - updated = await this.flushCard("complete", answerText, isError, segments); + cardUpdated = await this.flushCard("complete", answerText, isError, segments); } else if (this.currentMessageId !== null) { - updated = await this.flushCard("complete", "", isError, []); + cardUpdated = await this.flushCard("complete", "", isError, []); } - if (!updated) { + if (!cardUpdated) { // Card path failed (e.g. residual content policy). Deliver text + standalone images. - updated = await this.deliverPlainFallback(segments, answerText); + await this.deliverPlainFallback(segments, answerText); } - if (!updated && this.interrupted) { + // Interrupt is terminal; if the live card could not be finalized, always + // send an explicit notice so the teacher sees the abort even when plain + // text partial delivery succeeded. + if (!cardUpdated && this.interrupted) { await sendText(this.rt, this.chatId, "\u5DF2\u4E2D\u65AD\u5F53\u524D\u8FD0\u884C\u3002", this.sendOptions); } } finally { diff --git a/hub/test/integration/trigger.test.ts b/hub/test/integration/trigger.test.ts index ab855c2..73ef63d 100644 --- a/hub/test/integration/trigger.test.ts +++ b/hub/test/integration/trigger.test.ts @@ -1611,7 +1611,9 @@ describe("trigger full lifecycle (integration)", () => { expect(runs[0]?.status).toBe("CANCELED"); }); expect(patch).toHaveBeenCalled(); - expect(rt.sentTexts).toContain("已中断当前运行。"); + await vi.waitFor(() => { + expect(rt.sentTexts).toContain("已中断当前运行。"); + }); }); it("denies interrupt when the operator lacks agent.cancel permission", async () => {