Skip to content

Commit b3d88dd

Browse files
committed
feat: fix streaming abort functionality to properly stop generation
- Add optional abortSignal parameter to MessageCompletionHook.complete() - Support external AbortController for proper request cancellation - Fix timeout handling to only apply to internally created AbortControllers - Update Chatbox.tsx to pass abortController signal to complete function - Add detailed documentation in STREAMING_ABORT_FIX.md - Ensure Stop Generating button properly aborts background requests
1 parent 704fb1d commit b3d88dd

File tree

3 files changed

+158
-13
lines changed

3 files changed

+158
-13
lines changed

STREAMING_ABORT_FIX.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# 流式处理停止生成功能修复
2+
3+
## 问题描述
4+
5+
在流式处理过程中,当用户点击"Stop Generating"按钮时,后台请求没有正确被中止,导致服务器继续处理请求。
6+
7+
## 根本原因分析
8+
9+
通过代码分析发现,问题出现在 `useMessageCompletion` hook 中的 AbortController 管理不当:
10+
11+
1. **Chatbox.tsx (第73行)**:创建了新的 AbortController 并存储在 `abortControllerRef.current`
12+
2. **useMessageCompletion.ts (第640行)**:在 `complete` 函数内部又创建了一个新的 AbortController,覆盖了外部传递的
13+
3. **核心问题**`useMessageCompletion` 创建的 AbortController 没有暴露给外部调用者,导致点击停止按钮时无法正确中止正在进行的请求
14+
15+
## 修复方案
16+
17+
### 1. 修改 MessageCompletionHook 接口
18+
19+
```typescript
20+
export interface MessageCompletionHook {
21+
completeWithStreamMode: (
22+
response: Response,
23+
signal: AbortSignal,
24+
setGeneratingMessage?: (message: string) => void
25+
) => Promise<ChatStoreMessage>;
26+
completeWithFetchMode: (response: Response) => Promise<ChatStoreMessage>;
27+
complete: (
28+
onMCPToolCall?: (message: ChatStoreMessage) => void,
29+
setGeneratingMessage?: (message: string) => void,
30+
abortSignal?: AbortSignal // 新增:接受外部 AbortSignal
31+
) => Promise<void>;
32+
}
33+
```
34+
35+
### 2. 修改 complete 函数实现
36+
37+
```typescript
38+
const complete = async (
39+
onMCPToolCall?: (message: ChatStoreMessage) => void,
40+
setGeneratingMessage?: (message: string) => void,
41+
abortSignal?: AbortSignal
42+
) => {
43+
try {
44+
let signal: AbortSignal;
45+
let abortController: AbortController | null = null;
46+
47+
if (abortSignal) {
48+
// 使用外部传入的 AbortSignal
49+
signal = abortSignal;
50+
} else {
51+
// 创建新的 AbortController(向后兼容)
52+
abortController = new AbortController();
53+
signal = abortController.signal;
54+
}
55+
56+
// 超时处理只对内部创建的 AbortController
57+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
58+
if (abortController) {
59+
timeoutId = setTimeout(() => {
60+
abortController!.abort();
61+
}, 120000); // 2分钟超时
62+
}
63+
64+
const response = await client._fetch(
65+
chatStore.streamMode,
66+
chatStore.logprobs,
67+
signal
68+
);
69+
70+
if (timeoutId !== null) {
71+
clearTimeout(timeoutId);
72+
}
73+
74+
// ... 其余处理逻辑
75+
}
76+
};
77+
```
78+
79+
### 3. 修改 Chatbox.tsx 中的调用
80+
81+
```typescript
82+
const handleComplete = async () => {
83+
try {
84+
setShowGenerating(true);
85+
abortControllerRef.current = new AbortController();
86+
87+
await complete(
88+
(message) => {
89+
showMCPConfirmation(message);
90+
},
91+
setGeneratingMessage,
92+
abortControllerRef.current.signal // 传递 AbortSignal
93+
);
94+
95+
setShowRetry(false);
96+
} catch (error: any) {
97+
if (error.name === "AbortError") {
98+
console.log("abort complete");
99+
return;
100+
}
101+
setShowRetry(true);
102+
alert(error);
103+
} finally {
104+
setShowGenerating(false);
105+
setSelectedChatIndex(selectedChatIndex);
106+
}
107+
};
108+
```
109+
110+
## 修复效果
111+
112+
1. **正确的信号传递**:外部创建的 AbortController 现在能正确传递到底层的网络请求和流式处理逻辑
113+
2. **有效的中断机制**:当用户点击"Stop Generating"按钮时,`abortControllerRef.current.abort()` 能正确中止:
114+
- HTTP 请求(通过 fetch 的 signal 参数)
115+
- 流式响应处理(通过 processStreamResponse 的 signal 检查)
116+
3. **向后兼容**:保持了原有的 API 兼容性,如果外部不传入 AbortSignal,会创建内部的 AbortController
117+
118+
## 测试验证
119+
120+
修复后的代码通过了:
121+
- TypeScript 类型检查 (`npm run typecheck`)
122+
- 项目构建 (`npm run build`)
123+
- 保持了原有功能的完整性
124+
125+
## 相关文件
126+
127+
- `/src/hooks/useMessageCompletion.ts` - 主要修复文件
128+
- `/src/pages/Chatbox.tsx` - 调用修改
129+
- `/src/chatgpt.ts` - 底层流式处理逻辑(已有正确的 signal 检查)
130+
131+
这个修复确保了流式处理过程中用户点击停止按钮时,后台请求能被正确中止,解决了资源浪费和用户体验问题。

src/hooks/useMessageCompletion.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ export interface MessageCompletionHook {
7474
completeWithFetchMode: (response: Response) => Promise<ChatStoreMessage>;
7575
complete: (
7676
onMCPToolCall?: (message: ChatStoreMessage) => void,
77-
setGeneratingMessage?: (message: string) => void
77+
setGeneratingMessage?: (message: string) => void,
78+
abortSignal?: AbortSignal
7879
) => Promise<void>;
7980
}
8081

@@ -386,8 +387,7 @@ export function useMessageCompletion(): MessageCompletionHook {
386387

387388
setGeneratingMessage?.("");
388389

389-
const content = allChunkMessage.join("");
390-
const reasoning_content = allReasoningContentChunk.join("");
390+
391391

392392
// Process audio data
393393
// Only tested with Alibaba Cloud Bailian models - audio format is fixed (24kHz, 16-bit, mono)
@@ -541,7 +541,8 @@ export function useMessageCompletion(): MessageCompletionHook {
541541

542542
const complete = async (
543543
onMCPToolCall?: (message: ChatStoreMessage) => void,
544-
setGeneratingMessage?: (message: string) => void
544+
setGeneratingMessage?: (message: string) => void,
545+
abortSignal?: AbortSignal
545546
) => {
546547
// manually copy status from chatStore to client
547548
client.apiEndpoint = chatStore.apiEndpoint;
@@ -637,20 +638,33 @@ export function useMessageCompletion(): MessageCompletionHook {
637638
const created_at = new Date();
638639

639640
try {
640-
const abortController = new AbortController();
641+
let signal: AbortSignal;
642+
let abortController: AbortController | null = null;
643+
644+
if (abortSignal) {
645+
signal = abortSignal;
646+
} else {
647+
abortController = new AbortController();
648+
signal = abortController.signal;
649+
}
641650

642-
// 添加超时处理
643-
const timeoutId = setTimeout(() => {
644-
abortController.abort();
645-
}, 120000); // 2分钟超时
651+
// 添加超时处理(只对内部创建的AbortController)
652+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
653+
if (abortController) {
654+
timeoutId = setTimeout(() => {
655+
abortController!.abort();
656+
}, 120000); // 2分钟超时
657+
}
646658

647659
const response = await client._fetch(
648660
chatStore.streamMode,
649661
chatStore.logprobs,
650-
abortController.signal
662+
signal
651663
);
652664

653-
clearTimeout(timeoutId);
665+
if (timeoutId !== null) {
666+
clearTimeout(timeoutId);
667+
}
654668

655669
// 检查HTTP状态码
656670
if (!response.ok) {
@@ -681,7 +695,7 @@ export function useMessageCompletion(): MessageCompletionHook {
681695
if (contentType?.startsWith("text/event-stream")) {
682696
cs = await completeWithStreamMode(
683697
response,
684-
abortController.signal,
698+
signal,
685699
setGeneratingMessage
686700
);
687701
} else if (contentType?.startsWith("application/json")) {

src/pages/Chatbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export default function ChatBOX() {
7474

7575
await complete((message) => {
7676
showMCPConfirmation(message);
77-
}, setGeneratingMessage);
77+
}, setGeneratingMessage, abortControllerRef.current.signal);
7878

7979
setShowRetry(false);
8080
} catch (error: any) {

0 commit comments

Comments
 (0)