From ca8ebb1634ed09f200007766fcb56378fd3e4f25 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 18 Jun 2026 07:07:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20AIOps=E6=99=BA=E8=83=BD=E5=8A=A9?= =?UTF-8?q?=E6=89=8B=E6=94=AF=E6=8C=81=E5=81=9C=E6=AD=A2=E7=94=9F=E6=88=90?= =?UTF-8?q?(AbortController=E4=B8=AD=E6=AD=A2=E6=B5=81=E5=BC=8F,=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E9=83=A8=E5=88=86=E5=86=85=E5=AE=B9+=E5=B7=B2?= =?UTF-8?q?=E5=81=9C=E6=AD=A2=E6=8F=90=E7=A4=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/aiops/AIOpsAssistant.tsx | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/aiops/AIOpsAssistant.tsx b/frontend/src/components/aiops/AIOpsAssistant.tsx index 3be4f7d..d72f8e0 100644 --- a/frontend/src/components/aiops/AIOpsAssistant.tsx +++ b/frontend/src/components/aiops/AIOpsAssistant.tsx @@ -40,6 +40,7 @@ import { Loader2, CheckCircle2, Wifi, + Square, } from 'lucide-react'; // LLM 提供商列表(OpenAI 兼容接口) // Logan.Li @@ -133,6 +134,7 @@ interface Message { content: string; timestamp: string; isError?: boolean; + isStopped?: boolean; } // 格式化时间为 HH:MM // Logan.Li @@ -145,6 +147,10 @@ export function AIOpsAssistant() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); + // 用于中止流式生成(点击"停止"按钮时 abort) // Logan.Li + const abortRef = useRef(null); + // 组件卸载时中止进行中的流式请求,避免 setState on unmounted // Logan.Li + useEffect(() => () => abortRef.current?.abort(), []); const [isConfigOpen, setIsConfigOpen] = useState(false); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); @@ -251,6 +257,9 @@ export function AIOpsAssistant() { ]; setMessages(newMessages); setIsLoading(true); + // 创建 AbortController 以支持\"停止生成\" // Logan.Li + const controller = new AbortController(); + abortRef.current = controller; // 用于在收到第一个 chunk 前显示"思考中"占位 let firstChunkArrived = false; try { @@ -266,22 +275,38 @@ export function AIOpsAssistant() { m.id === assistantId ? { ...m, content: m.content + delta } : m, ), ); - }); + }, controller.signal); } catch (e) { - const errMsg = e instanceof Error ? e.message : '对话失败'; - setMessages((prev) => - prev.map((m) => - m.id === assistantId - ? { - ...m, - content: firstChunkArrived ? m.content + `\n\n⚠️ ${errMsg}` : `⚠️ ${errMsg}`, - isError: !firstChunkArrived, - } - : m, - ), - ); + // 用户主动中止:保留已生成的部分内容,不当作错误 // Logan.Li + if (controller.signal.aborted) { + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + content: m.content || '(已停止生成)', + isStopped: true, + } + : m, + ), + ); + } else { + const errMsg = e instanceof Error ? e.message : '对话失败'; + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + content: firstChunkArrived ? m.content + `\n\n⚠️ ${errMsg}` : `⚠️ ${errMsg}`, + isError: !firstChunkArrived, + } + : m, + ), + ); + } } finally { setIsLoading(false); + abortRef.current = null; } }; @@ -485,7 +510,10 @@ export function AIOpsAssistant() { )} -
{fmtTime(message.timestamp)}
+
+ {fmtTime(message.timestamp)} + {message.isStopped && (已停止)} +
); @@ -508,6 +536,17 @@ export function AIOpsAssistant() { + {/* 流式生成中显示\"停止生成\"按钮,中止当前请求 // Logan.Li */} + {isLoading && ( + + )}