fix(hub): always send interrupt notice when card finalize fails

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.
This commit is contained in:
2026-07-30 15:02:08 +08:00
parent ee928b5832
commit ace724c609
2 changed files with 12 additions and 7 deletions
+9 -6
View File
@@ -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 {
+2
View File
@@ -1611,8 +1611,10 @@ describe("trigger full lifecycle (integration)", () => {
expect(runs[0]?.status).toBe("CANCELED");
});
expect(patch).toHaveBeenCalled();
await vi.waitFor(() => {
expect(rt.sentTexts).toContain("已中断当前运行。");
});
});
it("denies interrupt when the operator lacks agent.cancel permission", async () => {
// EDIT role can trigger but cannot cancel (agent.cancel requires MANAGE).