From 5823fbfa7297ee8505b448547da1f97b39debbc3 Mon Sep 17 00:00:00 2001
From: MT-Mint <798521692@qq.com>
Date: Sat, 11 Apr 2026 14:39:38 +0800
Subject: [PATCH] =?UTF-8?q?fix(threads):=20=E5=BF=BD=E7=95=A5=E6=B5=81?=
=?UTF-8?q?=E5=8F=96=E6=B6=88=E5=AF=BC=E8=87=B4=E7=9A=84=E9=94=99=E8=AF=AF?=
=?UTF-8?q?=E6=8F=90=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
识别 cancelled/canceled/abort 等取消信号。\n在流式请求被主动停止或中断时不再弹出错误 toast,减少误报。
---
.../components/workspace/workspace-header.tsx | 2 +-
frontend/src/core/threads/hooks.ts | 26 +++++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/workspace/workspace-header.tsx b/frontend/src/components/workspace/workspace-header.tsx
index 036f9927..87a7272c 100644
--- a/frontend/src/components/workspace/workspace-header.tsx
+++ b/frontend/src/components/workspace/workspace-header.tsx
@@ -43,7 +43,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
) : (
{/* TODO: 测试标识 */}
- XClaw v3.2.3
+ XClaw v3.2.4
)}
diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts
index c2bb5abe..08e712fe 100644
--- a/frontend/src/core/threads/hooks.ts
+++ b/frontend/src/core/threads/hooks.ts
@@ -49,6 +49,12 @@ export type LegacyThreadStreamOptions = {
const STREAM_ERROR_FALLBACK_MESSAGE = "Request failed.";
const STREAM_ERROR_TOAST_MESSAGE = "出现了某些错误。";
const STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS = 2000;
+const STREAM_CANCEL_PATTERNS = [
+ /\bcancellederror\b/i,
+ /\bcancelled\b/i,
+ /\bcanceled\b/i,
+ /\babort(?:ed|error)?\b/i,
+];
function readMessageCandidate(value: unknown): string | null {
if (typeof value === "string" && value.trim()) {
@@ -116,6 +122,21 @@ function getStreamErrorMessage(error: unknown): string {
return STREAM_ERROR_FALLBACK_MESSAGE;
}
+function isStreamCancellation(error: unknown, message: string): boolean {
+ const direct =
+ typeof error === "object" &&
+ error !== null &&
+ "name" in error &&
+ typeof Reflect.get(error, "name") === "string"
+ ? String(Reflect.get(error, "name"))
+ : "";
+
+ const candidates = [message, direct];
+ return candidates.some((value) =>
+ STREAM_CANCEL_PATTERNS.some((pattern) => pattern.test(value)),
+ );
+}
+
function normalizeThreadId(
value: string | null | undefined,
): string | undefined {
@@ -234,6 +255,11 @@ export function useThreadStream({
const showStreamErrorToast = useCallback((error: unknown) => {
const message = getStreamErrorMessage(error);
+ if (isStreamCancellation(error, message)) {
+ // Cancellation is expected when user presses "Stop" or stream disconnects.
+ console.info("[useThreadStream] stream cancelled:", message);
+ return;
+ }
const now = Date.now();
const lastToast = lastErrorToastRef.current;
if (