- {{ extractContentFromString(message.content) }}
+
+
+
+
{{ item.text }}
+
{{ item.text }}
+
+
+
+
+
+ {{ extractContentFromString(message.content) }}
+
@@ -82,7 +106,8 @@ export default {
scrollTimer: null,
isFirstLoad: true,
playingAudioId: null,
- audioElement: null
+ audioElement: null,
+ expandedToolResults: {} // 跟踪工具结果的展开状态
};
},
watch: {
@@ -159,6 +184,13 @@ export default {
// 尝试解析为 JSON
try {
const jsonObj = JSON.parse(content);
+
+ // 如果是数组格式(包含 text 和 tool)
+ if (Array.isArray(jsonObj)) {
+ return jsonObj;
+ }
+
+ // 如果是对象且有 content 字段
if (jsonObj && typeof jsonObj === 'object' && jsonObj.content) {
return jsonObj.content;
}
@@ -169,6 +201,23 @@ export default {
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
return content;
},
+ // 切换工具结果的展开/折叠状态
+ toggleToolResult(messageIndex, itemIndex) {
+ const key = `${messageIndex}-${itemIndex}`;
+ this.$set(this.expandedToolResults, key, !this.expandedToolResults[key]);
+ },
+ // 判断工具结果是否处于折叠状态
+ isToolResultCollapsed(messageIndex, itemIndex) {
+ const key = `${messageIndex}-${itemIndex}`;
+ // 默认折叠(true表示折叠)
+ return !this.expandedToolResults[key];
+ },
+ // 获取截断的文本(只显示第一行)
+ getFirstLineText(text) {
+ if (!text) return '';
+ const firstLine = text.split('\n')[0];
+ return firstLine.length < text.length ? firstLine + '...' : text;
+ },
resetData() {
this.sessions = [];
this.messages = [];
@@ -178,6 +227,7 @@ export default {
this.loading = false;
this.hasMore = true;
this.isFirstLoad = true;
+ this.expandedToolResults = {};
},
handleClose() {
this.dialogVisible = false;
@@ -450,6 +500,56 @@ export default {
color: white;
}
+.content-wrapper {
+ width: 100%;
+}
+
+.text-content {
+ display: block;
+ margin-bottom: 4px;
+}
+
+.tool-call-text {
+ color: #1890ff;
+ font-family: 'Courier New', monospace;
+ font-weight: 500;
+ font-size: 12px;
+ display: block;
+ margin-top: 4px;
+}
+
+.user-message .tool-call-text {
+ color: #e6f7ff;
+}
+
+.tool-message .message-content {
+ background-color: #f0f0f0;
+}
+
+.tool-result-wrapper {
+ position: relative;
+ padding-right: 20px;
+}
+
+.tool-result-collapsed {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tool-toggle-btn {
+ position: absolute;
+ right: 0;
+ top: 0;
+ cursor: pointer;
+ color: #1890ff;
+ font-size: 12px;
+}
+
+.tool-toggle-btn:hover {
+ color: #40a9ff;
+}
+
.loading,
.no-more {
text-align: center;
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index 90c9704e..b707e82a 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -24,7 +24,7 @@ from core.utils.modules_initialize import (
initialize_tts,
initialize_asr,
)
-from core.handle.reportHandle import report
+from core.handle.reportHandle import report, enqueue_tool_report
from core.providers.tts.default import DefaultTTS
from concurrent.futures import ThreadPoolExecutor
from core.utils.dialogue import Message, Dialogue
@@ -1063,6 +1063,10 @@ class ConnectionHandler:
)
if not bHasError and len(tool_calls_list) > 0:
+ self.logger.bind(tag=TAG).debug(
+ f"检测到 {len(tool_calls_list)} 个工具调用"
+ )
+
# 更新工具调用统计
if depth == 0:
current_turn = len(self.dialogue.dialogue) // 2
@@ -1079,10 +1083,6 @@ class ConnectionHandler:
self.dialogue.put(Message(role="assistant", content=text_buff))
response_message.clear()
- self.logger.bind(tag=TAG).debug(
- f"检测到 {len(tool_calls_list)} 个工具调用"
- )
-
# 收集所有工具调用的 Future
futures_with_data = []
for tool_call_data in tool_calls_list:
@@ -1090,21 +1090,28 @@ class ConnectionHandler:
f"function_name={tool_call_data['name']}, function_id={tool_call_data['id']}, function_arguments={tool_call_data['arguments']}"
)
+ # 使用公共方法上报工具调用
+ tool_input = json.loads(tool_call_data.get("arguments") or "{}")
+ enqueue_tool_report(self, tool_call_data['name'], tool_input)
+
future = asyncio.run_coroutine_threadsafe(
self.func_handler.handle_llm_function_call(
self, tool_call_data
),
self.loop,
)
- futures_with_data.append((future, tool_call_data))
+ futures_with_data.append((future, tool_call_data, tool_input))
# 等待协程结束(实际等待时长为最慢的那个)
tool_results = []
- for future, tool_call_data in futures_with_data:
+ for future, tool_call_data, tool_input in futures_with_data:
result = future.result()
tool_results.append((result, tool_call_data))
- # 统一处理所有工具调用结果
+ # 使用公共方法上报工具调用结果
+ enqueue_tool_report(self, tool_call_data['name'], tool_input, str(result.result) if result.result else None, report_tool_call=False)
+
+ # 统一处理工具调用结果
if tool_results:
self._handle_function_result(tool_results, depth=depth)
diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py
index a9e3ee15..f092dfb2 100644
--- a/main/xiaozhi-server/core/handle/intentHandler.py
+++ b/main/xiaozhi-server/core/handle/intentHandler.py
@@ -10,6 +10,7 @@ from core.providers.tts.dto.dto import ContentType
from core.handle.helloHandle import checkWakeupWords
from plugins_func.register import Action, ActionResponse
from core.handle.sendAudioHandle import send_stt_message
+from core.handle.reportHandle import enqueue_tool_report
from core.utils.util import remove_punctuation_and_length
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType
@@ -141,6 +142,17 @@ async def process_intent_result(
await send_stt_message(conn, original_text)
conn.client_abort = False
+ # 准备工具调用参数
+ tool_input = {}
+ if function_args:
+ if isinstance(function_args, str):
+ tool_input = json.loads(function_args) if function_args else {}
+ elif isinstance(function_args, dict):
+ tool_input = function_args
+
+ # 上报工具调用
+ enqueue_tool_report(conn, function_name, tool_input)
+
# 使用executor执行函数调用和结果处理
def process_function_call():
conn.dialogue.put(Message(role="user", content=original_text))
@@ -159,7 +171,10 @@ async def process_intent_result(
action=Action.ERROR, result=str(e), response=str(e)
)
+ # 上报工具调用结果
if result:
+ enqueue_tool_report(conn, function_name, tool_input, str(result.result) if result.result else None, report_tool_call=False)
+
if result.action == Action.RESPONSE: # 直接回复前端
text = result.response
if text is not None:
diff --git a/main/xiaozhi-server/core/handle/reportHandle.py b/main/xiaozhi-server/core/handle/reportHandle.py
index b4f2d3f6..0f5be087 100644
--- a/main/xiaozhi-server/core/handle/reportHandle.py
+++ b/main/xiaozhi-server/core/handle/reportHandle.py
@@ -10,6 +10,7 @@ TTS上报功能已集成到ConnectionHandler类中。
"""
import time
+import json
import opuslib_next
from typing import TYPE_CHECKING
@@ -26,7 +27,7 @@ async def report(conn: "ConnectionHandler", type, text, opus_data, report_time):
Args:
conn: 连接对象
- type: 上报类型,1为用户,2为智能体
+ type: 上报类型,1为用户,2为智能体,3为工具调用
text: 合成文本
opus_data: opus音频数据
report_time: 上报时间
@@ -132,6 +133,45 @@ def enqueue_tts_report(conn: "ConnectionHandler", text, opus_data):
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
+def enqueue_tool_report(conn: "ConnectionHandler", tool_name: str, tool_input: dict, tool_result: str = None, report_tool_call: bool = True):
+ """将工具调用数据加入上报队列
+
+ Args:
+ conn: 连接对象
+ tool_name: 工具名称
+ tool_input: 工具输入参数
+ tool_result: 工具执行结果(可选)
+ report_tool_call: 是否上报工具调用本身,默认True;仅上报结果时设为False
+ """
+ if not conn.read_config_from_api or conn.need_bind:
+ return
+ if conn.chat_history_conf == 0:
+ return
+
+ try:
+ timestamp = int(time.time())
+
+ # 构建工具调用内容
+ if report_tool_call:
+ tool_text = json.dumps(
+ [
+ {
+ "type": "tool",
+ "text": f"{tool_name}({json.dumps(tool_input, ensure_ascii=False)})",
+ }
+ ]
+ )
+ conn.report_queue.put((3, tool_text, None, timestamp))
+
+ # 构建工具结果内容
+ if tool_result:
+ result_display = f'{{"result":"{str(tool_result)}"}}'
+ result_content = json.dumps([{"type": "tool_result", "text": result_display}], ensure_ascii=False)
+ conn.report_queue.put((3, result_content, None, timestamp + 1))
+ except Exception as e:
+ conn.logger.bind(tag=TAG).error(f"加入工具上报队列失败: {e}")
+
+
def enqueue_asr_report(conn: "ConnectionHandler", text, opus_data):
if not conn.read_config_from_api or conn.need_bind or not conn.report_asr_enable:
return
diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py
index 21cda9e6..3d60d253 100644
--- a/main/xiaozhi-server/core/handle/sendAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py
@@ -318,3 +318,13 @@ async def send_stt_message(conn: "ConnectionHandler", text):
await send_tts_message(conn, "start")
# 发送start消息后客户端状态会处于说话中状态,同步服务端状态
conn.client_is_speaking = True
+
+
+async def send_display_message(conn: "ConnectionHandler", text):
+ """发送纯显示消息"""
+ message = {
+ "type": "stt",
+ "text": text,
+ "session_id": conn.session_id
+ }
+ await conn.websocket.send(json.dumps(message))
diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py
index 0b37c04d..986dd275 100644
--- a/main/xiaozhi-server/core/providers/asr/base.py
+++ b/main/xiaozhi-server/core/providers/asr/base.py
@@ -168,10 +168,10 @@ class ASRProviderBase(ABC):
self.stop_ws_connection()
if text_len > 0:
- # 使用自定义模块进行上报
- await startToChat(conn, enhanced_text)
audio_snapshot = asr_audio_task.copy()
enqueue_asr_report(conn, enhanced_text, audio_snapshot)
+ # 使用自定义模块进行上报
+ await startToChat(conn, enhanced_text)
except Exception as e:
logger.bind(tag=TAG).error(f"处理语音停止失败: {e}")
import traceback
diff --git a/main/xiaozhi-server/core/providers/tools/unified_tool_handler.py b/main/xiaozhi-server/core/providers/tools/unified_tool_handler.py
index 6f289632..a5d4c3d8 100644
--- a/main/xiaozhi-server/core/providers/tools/unified_tool_handler.py
+++ b/main/xiaozhi-server/core/providers/tools/unified_tool_handler.py
@@ -13,6 +13,7 @@ from .server_mcp import ServerMCPExecutor
from .device_iot import DeviceIoTExecutor
from .device_mcp import DeviceMCPExecutor
from .mcp_endpoint import MCPEndpointExecutor
+from core.handle.sendAudioHandle import send_display_message
class UnifiedToolHandler:
@@ -167,6 +168,12 @@ class UnifiedToolHandler:
self.logger.debug(f"调用函数: {function_name}, 参数: {arguments}")
+ # 发送工具调用显示消息到设备
+ try:
+ await send_display_message(self.conn, f"% {function_name}")
+ except Exception as e:
+ self.logger.warning(f"发送工具调用显示消息失败: {e}")
+
# 执行工具调用
result = await self.tool_manager.execute_tool(function_name, arguments)
return result
diff --git a/main/xiaozhi-server/plugins_func/functions/play_music.py b/main/xiaozhi-server/plugins_func/functions/play_music.py
index 835c3f50..1195ae54 100644
--- a/main/xiaozhi-server/plugins_func/functions/play_music.py
+++ b/main/xiaozhi-server/plugins_func/functions/play_music.py
@@ -217,7 +217,6 @@ async def play_local_music(conn: "ConnectionHandler", specific_file=None):
conn.logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
return
text = _get_random_play_prompt(selected_music)
- await send_stt_message(conn, text)
conn.dialogue.put(Message(role="assistant", content=text))
if conn.intent_type == "intent_llm":