From 2eaa5bbb11fcd865c00ed7fd3eea54ce1e831e01 Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Fri, 8 May 2026 10:29:50 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix=EF=BC=9A=E4=BC=98=E5=8C=96=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E8=BF=87=E7=A8=8B=E4=B8=AD=E5=BC=82=E5=B8=B8=E7=AC=A6?= =?UTF-8?q?=E5=8F=B7=E8=AE=B0=E5=BD=95=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 66 +++++++++++++++++++++----- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index d130bc81..916b2d61 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -2,6 +2,7 @@ import os import sys import copy import json +import re import uuid import time import queue @@ -1009,21 +1010,25 @@ class ConnectionHandler: self._merge_tool_calls(tool_calls_list, tools_call) # 流式提取 direct_answer 的 response 参数,实时送 TTS + # 使用安全缓冲区,防止 JSON 闭合符号泄漏到 TTS + _DA_STREAM_BUFFER = 5 for tc in tool_calls_list: if tc["name"] == "direct_answer" and tc.get("arguments"): da_text = self._extract_direct_answer_response(tc["arguments"]) sent_len = tc.get("_da_sent", 0) if da_text and len(da_text) > sent_len: - new_part = da_text[sent_len:] - tc["_da_sent"] = len(da_text) - self.tts.tts_text_queue.put( - TTSMessageDTO( - sentence_id=current_sentence_id, - sentence_type=SentenceType.MIDDLE, - content_type=ContentType.TEXT, - content_detail=new_part, + safe_end = max(sent_len, len(da_text) - _DA_STREAM_BUFFER) + if safe_end > sent_len: + new_part = da_text[sent_len:safe_end] + tc["_da_sent"] = safe_end + self.tts.tts_text_queue.put( + TTSMessageDTO( + sentence_id=current_sentence_id, + sentence_type=SentenceType.MIDDLE, + content_type=ContentType.TEXT, + content_detail=new_part, + ) ) - ) else: content = response @@ -1105,9 +1110,24 @@ class ConnectionHandler: f"模型选择 direct_answer,流式已播报,写入对话历史" ) for tc in direct_answer_calls: - # 文本已在流式阶段实时播报,这里只写入对话历史 da_response = self._extract_direct_answer_response(tc.get("arguments", "{}")) if da_response: + # 刷新流式缓冲区中未发送的部分 + sent_len = tc.get("_da_sent", 0) + remaining = da_response[sent_len:] + if remaining: + remaining = self._clean_trailing_json_garbage(remaining) + if remaining: + self.tts.tts_text_queue.put( + TTSMessageDTO( + sentence_id=current_sentence_id, + sentence_type=SentenceType.MIDDLE, + content_type=ContentType.TEXT, + content_detail=remaining, + ) + ) + # 写入对话历史 + da_response = self._clean_trailing_json_garbage(da_response) self.tts.store_tts_text(current_sentence_id, da_response) self.dialogue.put(Message(role="assistant", content=da_response)) @@ -1543,15 +1563,21 @@ class ConnectionHandler: @staticmethod def _extract_direct_answer_response(arguments_str): """从 direct_answer 的参数中提取 response 值。 - 支持完整的 JSON 和流式中不完整的 JSON 片段。 + 优先使用 json.loads 标准解析,流式阶段 fallback 到字符串提取。 """ if not arguments_str: return "" - # 找到 "response": " 的位置 + # 优先尝试标准 JSON 解析(适用于完整且格式正确的 JSON) + try: + data = json.loads(arguments_str) + if isinstance(data, dict) and "response" in data: + return data["response"] + except (json.JSONDecodeError, TypeError): + pass + # Fallback:流式阶段 JSON 可能不完整,使用字符串提取 marker = '"response": "' idx = arguments_str.find(marker) if idx < 0: - # 尝试不带空格的格式 marker = '"response":"' idx = arguments_str.find(marker) if idx < 0: @@ -1567,6 +1593,20 @@ class ConnectionHandler: raw = raw.replace('\\"', '"').replace('\\n', '\n').replace('\\\\', '\\') return raw + @staticmethod + def _clean_trailing_json_garbage(text): + """清理 response 末尾可能泄漏的 JSON 闭合符号。 + 模型有时会在 response 内容中生成 JSON 闭合字符(如 )"}} 或 '}), + 这些不是故事内容的一部分,需要去除。 + """ + if not text: + return text + # 匹配末尾的 JSON 闭合垃圾模式: + # 可选的中文标点 )'),紧跟 " 和 } 的组合,中间可夹杂空白/换行 + # 例如:)"}} '}" }} "}} 等 + cleaned = re.sub(r'[\s\n]*[)\)]?[\s\n]*["\'}\]]{1,6}$', '', text) + return cleaned if cleaned else text + def _merge_tool_calls(self, tool_calls_list, tools_call): """合并工具调用列表 From 7dae67a57b95ce319f023075bc3215286116b8ae Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Fri, 8 May 2026 10:46:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix=EF=BC=9A=E4=BC=98=E5=8C=96=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E8=BF=87=E7=A8=8B=E4=B8=ADJSON=E9=97=AD=E5=90=88?= =?UTF-8?q?=E7=AC=A6=E5=8F=B7=E6=B8=85=E7=90=86=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 47 ++++++++++++++++---------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 916b2d61..d5a0dae4 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -1020,15 +1020,18 @@ class ConnectionHandler: safe_end = max(sent_len, len(da_text) - _DA_STREAM_BUFFER) if safe_end > sent_len: new_part = da_text[sent_len:safe_end] - tc["_da_sent"] = safe_end - self.tts.tts_text_queue.put( - TTSMessageDTO( - sentence_id=current_sentence_id, - sentence_type=SentenceType.MIDDLE, - content_type=ContentType.TEXT, - content_detail=new_part, + # 清理 delta 中可能泄漏的 JSON 闭合垃圾 + new_part = self._clean_response_garbage(new_part) + if new_part: + tc["_da_sent"] = safe_end + self.tts.tts_text_queue.put( + TTSMessageDTO( + sentence_id=current_sentence_id, + sentence_type=SentenceType.MIDDLE, + content_type=ContentType.TEXT, + content_detail=new_part, + ) ) - ) else: content = response @@ -1116,7 +1119,7 @@ class ConnectionHandler: sent_len = tc.get("_da_sent", 0) remaining = da_response[sent_len:] if remaining: - remaining = self._clean_trailing_json_garbage(remaining) + remaining = self._clean_response_garbage(remaining) if remaining: self.tts.tts_text_queue.put( TTSMessageDTO( @@ -1127,7 +1130,7 @@ class ConnectionHandler: ) ) # 写入对话历史 - da_response = self._clean_trailing_json_garbage(da_response) + da_response = self._clean_response_garbage(da_response) self.tts.store_tts_text(current_sentence_id, da_response) self.dialogue.put(Message(role="assistant", content=da_response)) @@ -1594,18 +1597,26 @@ class ConnectionHandler: return raw @staticmethod - def _clean_trailing_json_garbage(text): - """清理 response 末尾可能泄漏的 JSON 闭合符号。 - 模型有时会在 response 内容中生成 JSON 闭合字符(如 )"}} 或 '}), + def _clean_response_garbage(text): + """清理 response 中可能泄漏的 JSON 闭合符号。 + 模型有时会在 response 内容中生成 JSON 闭合字符(如 )"}} 或 '}), 这些不是故事内容的一部分,需要去除。 """ if not text: return text - # 匹配末尾的 JSON 闭合垃圾模式: - # 可选的中文标点 )'),紧跟 " 和 } 的组合,中间可夹杂空白/换行 - # 例如:)"}} '}" }} "}} 等 - cleaned = re.sub(r'[\s\n]*[)\)]?[\s\n]*["\'}\]]{1,6}$', '', text) - return cleaned if cleaned else text + # 清理独立一行的 JSON 闭合垃圾(如 )"}} '}} "}} }} } ) + _garbage_chars = frozenset('")\'})') + lines = text.split('\n') + cleaned = [] + for line in lines: + stripped = line.strip() + if stripped and len(stripped) <= 8 and all(c in _garbage_chars for c in stripped): + continue + cleaned.append(line) + result = '\n'.join(cleaned) + # 清理末尾残留的 JSON 闭合符号 + result = re.sub(r'["\'}\]]+$', '', result.rstrip()).rstrip() + return result def _merge_tool_calls(self, tool_calls_list, tools_call): """合并工具调用列表