From dabd892601a4f272caca05dae20a1a38bf94b07f Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Tue, 28 Jul 2026 22:01:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(aibox/client):=20=E6=94=BE=E5=BC=83=20rich.?= =?UTF-8?q?Live=20=E6=94=B9=E4=B8=BA=20complete=20=E6=97=B6=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E6=80=A7=E9=9D=99=E6=80=81=E6=B8=B2=E6=9F=93=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=86=85=E5=AE=B9=E4=B8=80=E9=97=AA=E8=80=8C?= =?UTF-8?q?=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aibox/client.py | 77 +++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/aibox/client.py b/aibox/client.py index ceb002d..f65c22f 100644 --- a/aibox/client.py +++ b/aibox/client.py @@ -15,10 +15,9 @@ from typing import Optional, Dict, Any, List import websockets from colorama import init, Fore, Style as ColoramaStyle -# rich 为可选依赖:有则流式渲染 Markdown,无则回退纯文本流式输出 +# rich 为可选依赖:有则 AI 回复渲染为 Markdown,无则回退纯文本流式输出 try: from rich.console import Console - from rich.live import Live from rich.markdown import Markdown _RICH = True except ImportError: @@ -165,10 +164,9 @@ class SimpleCLI: self._manual_disconnect = False # 用户主动 /disconnect、/quit、/connect 后置 True,抑制自动重连 self._reconnect_task: Optional[asyncio.Task] = None - # Markdown 流式渲染状态(rich 可用时启用) + # Markdown 渲染(rich 可用时启用) self._console = Console() if _RICH else None - self._md_live = None # rich.Live 实例(渲染进行中) - self._md_buffer = "" # 当前累计的 AI 回复全文 + self._md_generating_shown = False # "生成中…"状态行是否已显示 # 流式输出状态 self.streaming_line = False # 是否正在流式输出行 @@ -194,7 +192,6 @@ class SimpleCLI: def _print_line(self, line: str = "", end: str = "\n"): """打印一行,并恢复提示符""" - self._md_stream_stop() # 打印普通行前,先定格进行中的 Markdown 渲染 with self.lock: current = self.current_input sys.stdout.write('\r' + ' ' * 120 + '\r') @@ -272,13 +269,9 @@ class SimpleCLI: if key in self.displayed_messages: return - # rich 可用:累计文本直接整体渲染为 Markdown(服务端 chunk 为累计全文) + # rich 可用:流式期间只显示状态行并累计全文,complete 时一次性渲染 if _RICH: - with self.lock: - self.waiting_for_response = True - self.prompt_shown = False - self.streaming_line = True - self._md_stream_update(clean) + self._md_show_generating() self.ai_previous = clean self.ai_full = clean return @@ -316,36 +309,31 @@ class SimpleCLI: self.ai_previous = clean self.ai_full = clean - # ── Markdown 流式渲染(rich)───────────────────────────────────────── - def _md_stream_start(self): - """开启 Live 渲染区域(若 rich 不可用或已开启则忽略)。""" - if not _RICH or self._md_live is not None: - return - self._md_buffer = "" - # transient=False:结束后保留最终渲染结果;vertical_overflow=visible 不裁剪长文 - self._md_live = Live(Markdown(""), console=self._console, - refresh_per_second=8, vertical_overflow="visible") - self._md_live.start() - - def _md_stream_update(self, full_text: str): - """用累计全文刷新渲染(服务端 chunk 即累计文本)。""" + # ── Markdown 渲染(rich)───────────────────────────────────────────── + def _md_show_generating(self): + """流式期间显示一次性状态行(不实时渲染,避免 Live 闪烁/被卷走)。""" if not _RICH: return - self._md_stream_start() - self._md_buffer = full_text - self._md_live.update(Markdown(full_text)) + with self.lock: + if not self._md_generating_shown: + sys.stdout.write('\n') + sys.stdout.write(f"{Colors.AI}🤖 AI助手: 生成中…{Colors.RESET}") + sys.stdout.flush() + self._md_generating_shown = True + self.streaming_line = True + self.prompt_shown = False - def _md_stream_stop(self): - """结束渲染:最终刷新一次后停住(内容定格保留在终端)。""" - if self._md_live is not None: - try: - if self._md_buffer: - self._md_live.update(Markdown(self._md_buffer)) - self._md_live.stop() - except Exception: - pass - finally: - self._md_live = None + def _md_render_final(self, text: str): + """complete 时一次性静态渲染最终 Markdown(先擦掉"生成中…"行)。""" + if not _RICH: + return + with self.lock: + if self._md_generating_shown: + sys.stdout.write('\r' + ' ' * 40 + '\r') + self._md_generating_shown = False + self.streaming_line = False + if text: + self._console.print(Markdown(text)) def _remember_displayed(self, key: str): """记录已显示消息(LRU:超 DISPLAYED_MAX 淘汰最旧,防长对话内存膨胀)。""" @@ -365,7 +353,6 @@ class SimpleCLI: elif msg_type == 'task_complete': self._handle_task_complete(extra) elif msg_type == 'dialogue_ended': - self._md_stream_stop() with self.lock: if self.streaming_line: sys.stdout.write('\n') @@ -398,7 +385,6 @@ class SimpleCLI: """处理流式块""" # 检查是否是工具调用相关的消息 if content.startswith('🔧 调用工具'): - self._md_stream_stop() # 工具消息与 Markdown 渲染区域交替,先定格 with self.lock: self.in_tool_phase = True self.prompt_shown = False @@ -412,7 +398,6 @@ class SimpleCLI: # 检查是否是工具结果 if content.startswith(('✅ 对话记忆已保存', '💾 保存记忆', '❌', '⚠️')): - self._md_stream_stop() with self.lock: # 打印工具结果 sys.stdout.write(f"{Colors.TASK}{content}{Colors.RESET}\n") @@ -437,20 +422,18 @@ class SimpleCLI: def _handle_complete(self, content: str): """处理完成事件 - 只负责结束流式输出,不重复显示内容""" - # rich 渲染路径:最终全文已在 Live 区域定格,只需收尾并恢复提示符 + # rich 渲染路径:擦掉"生成中…",一次性渲染最终 Markdown,恢复提示符 if _RICH: clean = self._clean_content(content) if content else "" + self._md_render_final(clean) if clean: - self._md_stream_update(clean) self._remember_displayed(clean[:200]) - self._md_stream_stop() with self.lock: self.in_tool_phase = False self.waiting_for_response = False self.streaming_line = False self.prompt_shown = True current = self.current_input - sys.stdout.write('\r' + ' ' * 120 + '\r') sys.stdout.write(self._get_prompt() + current) sys.stdout.flush() self.ai_previous = "" @@ -659,7 +642,6 @@ class SimpleCLI: else: self.display("system", "未连接") elif cmd == '/clear': - self._md_stream_stop() os.system('clear' if os.name == 'posix' else 'cls') self.displayed_messages.clear() self.current_input = "" @@ -731,7 +713,6 @@ class SimpleCLI: self.display("error", "未连接到服务器") return - self._md_stream_stop() # 发送新消息前定格上一段渲染 # 重置流式状态 with self.lock: if self.streaming_line: