From af5cd02663ac6e9e644c1d00faaeb1b77f32b628 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 1 Aug 2026 21:10:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(aibox/botops):=20=E6=80=9D=E8=80=83?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E6=8C=89=E6=97=B6=E9=97=B4=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E5=88=86=E5=B8=83=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E5=A0=86=E5=9C=A8=E9=A1=B6=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题根因: 1. ChatView.end_reasoning() 不清空 _reasoning_widget 引用,导致 多轮工具调用时 start_reasoning() 直接 return,所有轮次的思考 内容都被追加到第一个思考区域(在聊天区最顶部)。 2. AI widget 在工具调用期间不重置,导致后续轮次的思考区域被 挂载在AI widget中间,顺序混乱。 修复: 1. end_reasoning()/abort_reasoning() 更新标签后清除引用(widget 保留在DOM显示),下一轮 reasoning_start 创建新的思考区域。 2. 新增 ai_break 协议信号:服务端在工具调用执行前发送,前端 break_ai() 清除AI widget引用(保留已显示内容),后续chunk 创建新的AI widget,确保思考区域按时间顺序插入在两段AI文本之间。 3. ai_break 前flush chunk_buffer,防止工具调用前的最后文本片段丢失。 --- BotOps/app.py | 3 +++ BotOps/widgets/chat_view.py | 39 +++++++++++++++++++++++++++---------- aibox/aibox.py | 9 +++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/BotOps/app.py b/BotOps/app.py index 0b8f450..c3b62da 100644 --- a/BotOps/app.py +++ b/BotOps/app.py @@ -197,6 +197,9 @@ class BotOpsApp(App): else: # 🔧 调用工具: xxx / 工具输出 / 用户中断提示等 → 任务侧栏 self.tasks.write(stripped) + elif t == "ai_break": + # 工具调用前分段:结束当前AI文本段(保留已显示内容),后续chunk创建新widget + await self.chat.break_ai() elif t == "complete": # complete 携带完整全文,用于对齐已显示内容 await self.chat.finish_ai(content) diff --git a/BotOps/widgets/chat_view.py b/BotOps/widgets/chat_view.py index 4f0734e..bfcdfec 100644 --- a/BotOps/widgets/chat_view.py +++ b/BotOps/widgets/chat_view.py @@ -2,16 +2,16 @@ 服务端协议(aibox/aibox.py output_callback 原样透传 msg_type): - chunk 增量片段(正式回答内容),直接 append 到 AI 消息 - - reasoning_start 思考开始信号,创建思考区域(在AI回答之前) - - reasoning 思考内容增量片段,追加到思考区域 - - reasoning_end 思考结束信号 + - reasoning_start 思考开始信号,在当前末尾创建新的思考区域 + - reasoning 思考内容增量片段,追加到当前活跃的思考区域 + - reasoning_end 思考结束信号:更新标签后清除引用(widget保留显示), + 后续 reasoning_start 会创建新的思考区域 - complete 收尾信号:chunk 流式是内容权威来源,complete 仅用于收尾; 仅当流式完全丢帧(未建 widget 且无内容)时,用 full_text 补建 顺序保证:不预创建空AI widget,按消息到达顺序自然挂载。 - - 先到 reasoning_start → 思考区域挂在最后 - - 后到 chunk → AI回答挂在思考区域之后 - - 无思考直接 chunk → AI回答挂在最后 +多轮工具调用场景:每轮思考(reasoning_start→reasoning_end)创建独立的思考区域, + 按时间顺序分布在对话流中各自的AI回答之前,而非全部堆在顶部。 """ from textual.containers import VerticalScroll from textual.widgets import Markdown, Static @@ -89,17 +89,22 @@ class ChatView(VerticalScroll): self.call_after_refresh(self.scroll_end, animate=False) async def end_reasoning(self): - """结束思考区域,更新提示文字。""" + """结束当前思考区域,更新标签后清除引用(widget保留在界面上显示)。 + + 清除引用后,后续 reasoning_start 会在当前末尾位置创建新的思考区域, + 实现多轮工具调用时思考区域按时间顺序分布,而非全部堆在顶部。 + """ if self._reasoning_widget is not None and self._reasoning_text: - # 思考结束后保持展示,更新标签为已完成 display = self._reasoning_text if len(display) > 3000: display = "...(前略)\n" + display[-3000:] self._reasoning_widget.update(f"💭 思考过程({len(self._reasoning_text)}字符)\n{display}\n{'─' * 30}") - # 思考区域保留显示,不删除 + # 清除引用但保留widget在DOM中显示——下一轮reasoning_start会创建新widget + self._reasoning_widget = None + self._reasoning_text = "" async def abort_reasoning(self): - """中断思考区域(用户Ctrl+C),更新提示文字为已中断。""" + """中断当前思考区域,更新提示文字后清除引用。""" if self._reasoning_widget is not None: if self._reasoning_text: display = self._reasoning_text @@ -108,6 +113,9 @@ class ChatView(VerticalScroll): self._reasoning_widget.update(f"🛑 思考已中断({len(self._reasoning_text)}字符)\n{display}") else: self._reasoning_widget.update("🛑 思考已中断") + # 清除引用,已显示的widget保留 + self._reasoning_widget = None + self._reasoning_text = "" async def _ensure_ai_widget(self): """确保AI widget存在(第一次chunk到来时创建)。""" @@ -116,6 +124,17 @@ class ChatView(VerticalScroll): await self.mount(self._ai_widget) self._anchor_bottom() + async def break_ai(self): + """分段当前AI回答:清除引用但保留widget在DOM中显示。 + + 用于工具调用场景:工具调用前的AI文字(如"好的让我查一下")保留显示, + 后续chunk会创建新的AI widget,保证思考区域按时间顺序插入在两段AI文字之间。 + 与finish_ai不同:不设置_ai_finalized,对话继续中。 + """ + if self._ai_widget is not None: + self._ai_widget = None + self._ai_text = "" + async def append_ai(self, delta: str): """追加增量片段(服务端 chunk 为增量语义,累积文本后 update 整个 Markdown)。 diff --git a/aibox/aibox.py b/aibox/aibox.py index fa3e612..9992051 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -7424,6 +7424,15 @@ class DeepSeekChat: elif output_callback: output_callback("line", i18n.get('calling_tools') + "\n") + # 发送ai_break信号,通知前端分段当前AI文本段(保留已显示内容), + # 这样工具调用后新一轮的思考/回答会创建新的widget,按时间顺序排列 + if output_callback and chunk_buffer: + # 先flush剩余的chunk_buffer,确保最后片段不丢失 + output_callback("chunk", chunk_buffer) + chunk_buffer = "" + if output_callback: + output_callback("ai_break", "") + # 【关键修复1】先添加助手消息,包含 tool_calls assistant_msg = { "role": "assistant",