fix(BotOps): 修正 aibox 协议语义错配导致的输出丢失
- 服务端 chunk 为增量片段(chunk_buffer 发送后即清零),原 _delta
按累计全文假设计算,相邻 chunk 前缀撞字时静默丢字符;改为直接拼接
- complete 携带完整全文,finish_ai 改为全文对齐(不一致时整体替换,
可自愈丢帧/乱序),修复结尾全文重复 append
- 新增 line/reasoning/task_added/dialogue_reset/pong 消息路由:
line(🔧 工具调用提示、工具输出)不再落入兜底污染聊天区,改投任务侧栏
- 发送失败时移除预建的空 AI 消息框并提示,避免空等回复
- 断线提示补充重连窗口期服务端输出丢失说明
- 同步修正 ws_client/README 中的协议描述
This commit is contained in:
1 parent
f9cf408c82
commit
b86b089634
4 files changed
+76
-43
No files matched your search
+40
-31
@@ -1,20 +1,13 @@
|
||||
"""聊天视图:用户/AI/系统消息 + AI 回复流式 Markdown 渲染。"""
|
||||
"""聊天视图:用户/AI/系统消息 + AI 回复流式 Markdown 渲染。
|
||||
|
||||
服务端协议(aibox/main.py output_callback 原样透传 msg_type):
|
||||
- chunk 增量片段(服务端 chunk_buffer 发送后即清零),直接 append
|
||||
- complete 完整全文,用于对齐:与已显示文本不一致时整体替换(防丢帧/乱序)
|
||||
"""
|
||||
from textual.containers import VerticalScroll
|
||||
from textual.widgets import Markdown, Static
|
||||
|
||||
|
||||
def _delta(prev: str, new: str) -> str:
|
||||
"""计算累计文本的增量(服务端 chunk 为累计全文;前缀失配时取最长公共前缀后的部分)。"""
|
||||
if new.startswith(prev):
|
||||
return new[len(prev):]
|
||||
i = 0
|
||||
for a, b in zip(prev, new):
|
||||
if a != b:
|
||||
break
|
||||
i += 1
|
||||
return new[i:]
|
||||
|
||||
|
||||
class ChatView(VerticalScroll):
|
||||
"""可滚动聊天区。AI 回复用单个 Markdown 部件增量 append,实现流式渲染。
|
||||
|
||||
@@ -26,11 +19,11 @@ class ChatView(VerticalScroll):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._ai_widget: Markdown | None = None
|
||||
self._ai_prev = ""
|
||||
self._ai_text = ""
|
||||
|
||||
@property
|
||||
def ai_text(self) -> str:
|
||||
return self._ai_prev
|
||||
return self._ai_text
|
||||
|
||||
def _anchor_bottom(self):
|
||||
"""锚定吸底(需在布局刷新后执行,否则滚到的是旧位置)。"""
|
||||
@@ -48,34 +41,50 @@ class ChatView(VerticalScroll):
|
||||
"""预建一条 AI 消息(发送用户消息后即可调用,给出即时反馈)。"""
|
||||
if self._ai_widget is not None:
|
||||
return
|
||||
self._ai_prev = ""
|
||||
self._ai_text = ""
|
||||
self._ai_widget = Markdown("", classes="msg-ai")
|
||||
await self.mount(self._ai_widget)
|
||||
self._anchor_bottom()
|
||||
|
||||
async def append_ai(self, full_text: str):
|
||||
"""以累计全文增量渲染(服务端 chunk 为累计文本)。"""
|
||||
async def append_ai(self, delta: str):
|
||||
"""追加增量片段(服务端 chunk 为增量语义,直接拼接)。"""
|
||||
if not delta:
|
||||
return
|
||||
if self._ai_widget is None:
|
||||
await self.start_ai()
|
||||
d = _delta(self._ai_prev, full_text)
|
||||
if d:
|
||||
self._ai_widget.append(d)
|
||||
self._ai_prev = full_text
|
||||
# anchor 状态下容器会自动吸底;保险起见刷新后再滚一次
|
||||
self.call_after_refresh(self.scroll_end, animate=False)
|
||||
self._ai_widget.append(delta)
|
||||
self._ai_text += delta
|
||||
# anchor 状态下容器会自动吸底;保险起见刷新后再滚一次
|
||||
self.call_after_refresh(self.scroll_end, animate=False)
|
||||
|
||||
async def finish_ai(self, full_text: str = ""):
|
||||
"""收尾:用最终全文对齐一次(防中途丢帧),随后关闭本条 AI 消息。"""
|
||||
if self._ai_widget is None and full_text:
|
||||
await self.start_ai()
|
||||
if full_text and full_text != self._ai_prev:
|
||||
await self.append_ai(full_text)
|
||||
"""收尾一条 AI 消息。
|
||||
|
||||
- full_text 为 complete 携带的完整全文:与已显示不一致时整体替换对齐;
|
||||
- full_text 为空且未收到任何内容(如发送失败):移除预建的空消息框。
|
||||
"""
|
||||
w = self._ai_widget
|
||||
self._ai_widget = None
|
||||
self._ai_prev = ""
|
||||
if w is None:
|
||||
# 流式期间丢帧,只在 complete 收到全文:补建完整消息
|
||||
if full_text:
|
||||
self._ai_text = full_text
|
||||
await self.mount(Markdown(full_text, classes="msg-ai"))
|
||||
self._anchor_bottom()
|
||||
return
|
||||
if full_text and full_text != self._ai_text:
|
||||
# 已显示内容与全文不一致(丢字/乱序/丢帧),整体替换对齐
|
||||
await w.remove()
|
||||
self._ai_text = full_text
|
||||
await self.mount(Markdown(full_text, classes="msg-ai"))
|
||||
elif not full_text and not self._ai_text:
|
||||
# 预建后没有任何输出:移除空框
|
||||
await w.remove()
|
||||
self._ai_text = ""
|
||||
self.call_after_refresh(self.scroll_end, animate=False)
|
||||
|
||||
async def reset(self):
|
||||
"""清空聊天区(/clear)。"""
|
||||
self._ai_widget = None
|
||||
self._ai_prev = ""
|
||||
self._ai_text = ""
|
||||
await self.remove_children()
|
||||
Reference in new issue
Block a user