AstrBot 4.12.3__py3-none-any.whl → 4.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astrbot/builtin_stars/astrbot/process_llm_request.py +42 -1
- astrbot/builtin_stars/builtin_commands/commands/__init__.py +0 -2
- astrbot/builtin_stars/builtin_commands/commands/persona.py +68 -6
- astrbot/builtin_stars/builtin_commands/main.py +0 -26
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +91 -1
- astrbot/core/agent/tool.py +61 -20
- astrbot/core/astr_agent_hooks.py +3 -1
- astrbot/core/astr_agent_run_util.py +243 -1
- astrbot/core/astr_agent_tool_exec.py +2 -2
- astrbot/core/{sandbox → computer}/booters/base.py +4 -4
- astrbot/core/{sandbox → computer}/booters/boxlite.py +2 -2
- astrbot/core/computer/booters/local.py +234 -0
- astrbot/core/{sandbox → computer}/booters/shipyard.py +2 -2
- astrbot/core/computer/computer_client.py +102 -0
- astrbot/core/{sandbox → computer}/tools/__init__.py +2 -1
- astrbot/core/{sandbox → computer}/tools/fs.py +1 -1
- astrbot/core/computer/tools/python.py +94 -0
- astrbot/core/{sandbox → computer}/tools/shell.py +13 -5
- astrbot/core/config/default.py +90 -9
- astrbot/core/db/__init__.py +94 -1
- astrbot/core/db/po.py +46 -0
- astrbot/core/db/sqlite.py +248 -0
- astrbot/core/message/components.py +2 -2
- astrbot/core/persona_mgr.py +162 -2
- astrbot/core/pipeline/context_utils.py +2 -2
- astrbot/core/pipeline/preprocess_stage/stage.py +1 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +73 -6
- astrbot/core/pipeline/process_stage/utils.py +31 -4
- astrbot/core/pipeline/scheduler.py +1 -1
- astrbot/core/pipeline/waking_check/stage.py +0 -1
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +3 -3
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +32 -14
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +61 -2
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +57 -11
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +5 -7
- astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
- astrbot/core/platform/sources/webchat/webchat_event.py +24 -0
- astrbot/core/provider/manager.py +38 -0
- astrbot/core/provider/provider.py +54 -0
- astrbot/core/provider/sources/gemini_embedding_source.py +1 -1
- astrbot/core/provider/sources/gemini_source.py +12 -9
- astrbot/core/provider/sources/genie_tts.py +128 -0
- astrbot/core/provider/sources/openai_embedding_source.py +1 -1
- astrbot/core/skills/__init__.py +3 -0
- astrbot/core/skills/skill_manager.py +237 -0
- astrbot/core/star/command_management.py +1 -1
- astrbot/core/star/config.py +1 -1
- astrbot/core/star/context.py +9 -8
- astrbot/core/star/filter/command.py +1 -1
- astrbot/core/star/filter/custom_filter.py +2 -2
- astrbot/core/star/register/star_handler.py +2 -4
- astrbot/core/utils/astrbot_path.py +6 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/config.py +236 -2
- astrbot/dashboard/routes/live_chat.py +423 -0
- astrbot/dashboard/routes/persona.py +265 -1
- astrbot/dashboard/routes/skills.py +148 -0
- astrbot/dashboard/routes/util.py +102 -0
- astrbot/dashboard/server.py +21 -5
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/METADATA +1 -1
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/RECORD +69 -63
- astrbot/builtin_stars/builtin_commands/commands/tool.py +0 -31
- astrbot/core/sandbox/sandbox_client.py +0 -52
- astrbot/core/sandbox/tools/python.py +0 -74
- /astrbot/core/{sandbox → computer}/olayer/__init__.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/filesystem.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/python.py +0 -0
- /astrbot/core/{sandbox → computer}/olayer/shell.py +0 -0
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/WHEEL +0 -0
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.12.3.dist-info → astrbot-4.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
import wave
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import jwt
|
|
10
|
+
from quart import websocket
|
|
11
|
+
|
|
12
|
+
from astrbot import logger
|
|
13
|
+
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
|
14
|
+
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
|
|
15
|
+
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
16
|
+
|
|
17
|
+
from .route import Route, RouteContext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LiveChatSession:
|
|
21
|
+
"""Live Chat 会话管理器"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, session_id: str, username: str):
|
|
24
|
+
self.session_id = session_id
|
|
25
|
+
self.username = username
|
|
26
|
+
self.conversation_id = str(uuid.uuid4())
|
|
27
|
+
self.is_speaking = False
|
|
28
|
+
self.is_processing = False
|
|
29
|
+
self.should_interrupt = False
|
|
30
|
+
self.audio_frames: list[bytes] = []
|
|
31
|
+
self.current_stamp: str | None = None
|
|
32
|
+
self.temp_audio_path: str | None = None
|
|
33
|
+
|
|
34
|
+
def start_speaking(self, stamp: str):
|
|
35
|
+
"""开始说话"""
|
|
36
|
+
self.is_speaking = True
|
|
37
|
+
self.current_stamp = stamp
|
|
38
|
+
self.audio_frames = []
|
|
39
|
+
logger.debug(f"[Live Chat] {self.username} 开始说话 stamp={stamp}")
|
|
40
|
+
|
|
41
|
+
def add_audio_frame(self, data: bytes):
|
|
42
|
+
"""添加音频帧"""
|
|
43
|
+
if self.is_speaking:
|
|
44
|
+
self.audio_frames.append(data)
|
|
45
|
+
|
|
46
|
+
async def end_speaking(self, stamp: str) -> tuple[str | None, float]:
|
|
47
|
+
"""结束说话,返回组装的 WAV 文件路径和耗时"""
|
|
48
|
+
start_time = time.time()
|
|
49
|
+
if not self.is_speaking or stamp != self.current_stamp:
|
|
50
|
+
logger.warning(
|
|
51
|
+
f"[Live Chat] stamp 不匹配或未在说话状态: {stamp} vs {self.current_stamp}"
|
|
52
|
+
)
|
|
53
|
+
return None, 0.0
|
|
54
|
+
|
|
55
|
+
self.is_speaking = False
|
|
56
|
+
|
|
57
|
+
if not self.audio_frames:
|
|
58
|
+
logger.warning("[Live Chat] 没有音频帧数据")
|
|
59
|
+
return None, 0.0
|
|
60
|
+
|
|
61
|
+
# 组装 WAV 文件
|
|
62
|
+
try:
|
|
63
|
+
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
|
64
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
65
|
+
audio_path = os.path.join(temp_dir, f"live_audio_{uuid.uuid4()}.wav")
|
|
66
|
+
|
|
67
|
+
# 假设前端发送的是 PCM 数据,采样率 16000Hz,单声道,16位
|
|
68
|
+
with wave.open(audio_path, "wb") as wav_file:
|
|
69
|
+
wav_file.setnchannels(1) # 单声道
|
|
70
|
+
wav_file.setsampwidth(2) # 16位 = 2字节
|
|
71
|
+
wav_file.setframerate(16000) # 采样率 16000Hz
|
|
72
|
+
for frame in self.audio_frames:
|
|
73
|
+
wav_file.writeframes(frame)
|
|
74
|
+
|
|
75
|
+
self.temp_audio_path = audio_path
|
|
76
|
+
logger.info(
|
|
77
|
+
f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {os.path.getsize(audio_path)} bytes"
|
|
78
|
+
)
|
|
79
|
+
return audio_path, time.time() - start_time
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"[Live Chat] 组装 WAV 文件失败: {e}", exc_info=True)
|
|
83
|
+
return None, 0.0
|
|
84
|
+
|
|
85
|
+
def cleanup(self):
|
|
86
|
+
"""清理临时文件"""
|
|
87
|
+
if self.temp_audio_path and os.path.exists(self.temp_audio_path):
|
|
88
|
+
try:
|
|
89
|
+
os.remove(self.temp_audio_path)
|
|
90
|
+
logger.debug(f"[Live Chat] 已删除临时文件: {self.temp_audio_path}")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"[Live Chat] 删除临时文件失败: {e}")
|
|
93
|
+
self.temp_audio_path = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class LiveChatRoute(Route):
|
|
97
|
+
"""Live Chat WebSocket 路由"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
context: RouteContext,
|
|
102
|
+
db: Any,
|
|
103
|
+
core_lifecycle: AstrBotCoreLifecycle,
|
|
104
|
+
) -> None:
|
|
105
|
+
super().__init__(context)
|
|
106
|
+
self.core_lifecycle = core_lifecycle
|
|
107
|
+
self.db = db
|
|
108
|
+
self.plugin_manager = core_lifecycle.plugin_manager
|
|
109
|
+
self.sessions: dict[str, LiveChatSession] = {}
|
|
110
|
+
|
|
111
|
+
# 注册 WebSocket 路由
|
|
112
|
+
self.app.websocket("/api/live_chat/ws")(self.live_chat_ws)
|
|
113
|
+
|
|
114
|
+
async def live_chat_ws(self):
|
|
115
|
+
"""Live Chat WebSocket 处理器"""
|
|
116
|
+
# WebSocket 不能通过 header 传递 token,需要从 query 参数获取
|
|
117
|
+
# 注意:WebSocket 上下文使用 websocket.args 而不是 request.args
|
|
118
|
+
token = websocket.args.get("token")
|
|
119
|
+
if not token:
|
|
120
|
+
await websocket.close(1008, "Missing authentication token")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
jwt_secret = self.config["dashboard"].get("jwt_secret")
|
|
125
|
+
payload = jwt.decode(token, jwt_secret, algorithms=["HS256"])
|
|
126
|
+
username = payload["username"]
|
|
127
|
+
except jwt.ExpiredSignatureError:
|
|
128
|
+
await websocket.close(1008, "Token expired")
|
|
129
|
+
return
|
|
130
|
+
except jwt.InvalidTokenError:
|
|
131
|
+
await websocket.close(1008, "Invalid token")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
session_id = f"webchat_live!{username}!{uuid.uuid4()}"
|
|
135
|
+
live_session = LiveChatSession(session_id, username)
|
|
136
|
+
self.sessions[session_id] = live_session
|
|
137
|
+
|
|
138
|
+
logger.info(f"[Live Chat] WebSocket 连接建立: {username}")
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
while True:
|
|
142
|
+
message = await websocket.receive_json()
|
|
143
|
+
await self._handle_message(live_session, message)
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"[Live Chat] WebSocket 错误: {e}", exc_info=True)
|
|
147
|
+
|
|
148
|
+
finally:
|
|
149
|
+
# 清理会话
|
|
150
|
+
if session_id in self.sessions:
|
|
151
|
+
live_session.cleanup()
|
|
152
|
+
del self.sessions[session_id]
|
|
153
|
+
logger.info(f"[Live Chat] WebSocket 连接关闭: {username}")
|
|
154
|
+
|
|
155
|
+
async def _handle_message(self, session: LiveChatSession, message: dict):
|
|
156
|
+
"""处理 WebSocket 消息"""
|
|
157
|
+
msg_type = message.get("t") # 使用 t 代替 type
|
|
158
|
+
|
|
159
|
+
if msg_type == "start_speaking":
|
|
160
|
+
# 开始说话
|
|
161
|
+
stamp = message.get("stamp")
|
|
162
|
+
if not stamp:
|
|
163
|
+
logger.warning("[Live Chat] start_speaking 缺少 stamp")
|
|
164
|
+
return
|
|
165
|
+
session.start_speaking(stamp)
|
|
166
|
+
|
|
167
|
+
elif msg_type == "speaking_part":
|
|
168
|
+
# 音频片段
|
|
169
|
+
audio_data_b64 = message.get("data")
|
|
170
|
+
if not audio_data_b64:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# 解码 base64
|
|
174
|
+
import base64
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
audio_data = base64.b64decode(audio_data_b64)
|
|
178
|
+
session.add_audio_frame(audio_data)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f"[Live Chat] 解码音频数据失败: {e}")
|
|
181
|
+
|
|
182
|
+
elif msg_type == "end_speaking":
|
|
183
|
+
# 结束说话
|
|
184
|
+
stamp = message.get("stamp")
|
|
185
|
+
if not stamp:
|
|
186
|
+
logger.warning("[Live Chat] end_speaking 缺少 stamp")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
audio_path, assemble_duration = await session.end_speaking(stamp)
|
|
190
|
+
if not audio_path:
|
|
191
|
+
await websocket.send_json({"t": "error", "data": "音频组装失败"})
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# 处理音频:STT -> LLM -> TTS
|
|
195
|
+
await self._process_audio(session, audio_path, assemble_duration)
|
|
196
|
+
|
|
197
|
+
elif msg_type == "interrupt":
|
|
198
|
+
# 用户打断
|
|
199
|
+
session.should_interrupt = True
|
|
200
|
+
logger.info(f"[Live Chat] 用户打断: {session.username}")
|
|
201
|
+
|
|
202
|
+
async def _process_audio(
|
|
203
|
+
self, session: LiveChatSession, audio_path: str, assemble_duration: float
|
|
204
|
+
):
|
|
205
|
+
"""处理音频:STT -> LLM -> 流式 TTS"""
|
|
206
|
+
try:
|
|
207
|
+
# 发送 WAV 组装耗时
|
|
208
|
+
await websocket.send_json(
|
|
209
|
+
{"t": "metrics", "data": {"wav_assemble_time": assemble_duration}}
|
|
210
|
+
)
|
|
211
|
+
wav_assembly_finish_time = time.time()
|
|
212
|
+
|
|
213
|
+
session.is_processing = True
|
|
214
|
+
session.should_interrupt = False
|
|
215
|
+
|
|
216
|
+
# 1. STT - 语音转文字
|
|
217
|
+
ctx = self.plugin_manager.context
|
|
218
|
+
stt_provider = ctx.provider_manager.stt_provider_insts[0]
|
|
219
|
+
|
|
220
|
+
if not stt_provider:
|
|
221
|
+
logger.error("[Live Chat] STT Provider 未配置")
|
|
222
|
+
await websocket.send_json({"t": "error", "data": "语音识别服务未配置"})
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
await websocket.send_json(
|
|
226
|
+
{"t": "metrics", "data": {"stt": stt_provider.meta().type}}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
user_text = await stt_provider.get_text(audio_path)
|
|
230
|
+
if not user_text:
|
|
231
|
+
logger.warning("[Live Chat] STT 识别结果为空")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
logger.info(f"[Live Chat] STT 结果: {user_text}")
|
|
235
|
+
|
|
236
|
+
await websocket.send_json(
|
|
237
|
+
{
|
|
238
|
+
"t": "user_msg",
|
|
239
|
+
"data": {"text": user_text, "ts": int(time.time() * 1000)},
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# 2. 构造消息事件并发送到 pipeline
|
|
244
|
+
# 使用 webchat queue 机制
|
|
245
|
+
cid = session.conversation_id
|
|
246
|
+
queue = webchat_queue_mgr.get_or_create_queue(cid)
|
|
247
|
+
|
|
248
|
+
message_id = str(uuid.uuid4())
|
|
249
|
+
payload = {
|
|
250
|
+
"message_id": message_id,
|
|
251
|
+
"message": [{"type": "plain", "text": user_text}], # 直接发送文本
|
|
252
|
+
"action_type": "live", # 标记为 live mode
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# 将消息放入队列
|
|
256
|
+
await queue.put((session.username, cid, payload))
|
|
257
|
+
|
|
258
|
+
# 3. 等待响应并流式发送 TTS 音频
|
|
259
|
+
back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
|
|
260
|
+
|
|
261
|
+
bot_text = ""
|
|
262
|
+
audio_playing = False
|
|
263
|
+
|
|
264
|
+
while True:
|
|
265
|
+
if session.should_interrupt:
|
|
266
|
+
# 用户打断,停止处理
|
|
267
|
+
logger.info("[Live Chat] 检测到用户打断")
|
|
268
|
+
await websocket.send_json({"t": "stop_play"})
|
|
269
|
+
# 保存消息并标记为被打断
|
|
270
|
+
await self._save_interrupted_message(session, user_text, bot_text)
|
|
271
|
+
# 清空队列中未处理的消息
|
|
272
|
+
while not back_queue.empty():
|
|
273
|
+
try:
|
|
274
|
+
back_queue.get_nowait()
|
|
275
|
+
except asyncio.QueueEmpty:
|
|
276
|
+
break
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
result = await asyncio.wait_for(back_queue.get(), timeout=0.5)
|
|
281
|
+
except asyncio.TimeoutError:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
if not result:
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
result_message_id = result.get("message_id")
|
|
288
|
+
if result_message_id != message_id:
|
|
289
|
+
logger.warning(
|
|
290
|
+
f"[Live Chat] 消息 ID 不匹配: {result_message_id} != {message_id}"
|
|
291
|
+
)
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
result_type = result.get("type")
|
|
295
|
+
result_chain_type = result.get("chain_type")
|
|
296
|
+
data = result.get("data", "")
|
|
297
|
+
|
|
298
|
+
if result_chain_type == "agent_stats":
|
|
299
|
+
try:
|
|
300
|
+
stats = json.loads(data)
|
|
301
|
+
await websocket.send_json(
|
|
302
|
+
{
|
|
303
|
+
"t": "metrics",
|
|
304
|
+
"data": {
|
|
305
|
+
"llm_ttft": stats.get("time_to_first_token", 0),
|
|
306
|
+
"llm_total_time": stats.get("end_time", 0)
|
|
307
|
+
- stats.get("start_time", 0),
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"[Live Chat] 解析 AgentStats 失败: {e}")
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
if result_chain_type == "tts_stats":
|
|
316
|
+
try:
|
|
317
|
+
stats = json.loads(data)
|
|
318
|
+
await websocket.send_json(
|
|
319
|
+
{
|
|
320
|
+
"t": "metrics",
|
|
321
|
+
"data": stats,
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error(f"[Live Chat] 解析 TTSStats 失败: {e}")
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
if result_type == "plain":
|
|
329
|
+
# 普通文本消息
|
|
330
|
+
bot_text += data
|
|
331
|
+
|
|
332
|
+
elif result_type == "audio_chunk":
|
|
333
|
+
# 流式音频数据
|
|
334
|
+
if not audio_playing:
|
|
335
|
+
audio_playing = True
|
|
336
|
+
logger.debug("[Live Chat] 开始播放音频流")
|
|
337
|
+
|
|
338
|
+
# Calculate latency from wav assembly finish to first audio chunk
|
|
339
|
+
speak_to_first_frame_latency = (
|
|
340
|
+
time.time() - wav_assembly_finish_time
|
|
341
|
+
)
|
|
342
|
+
await websocket.send_json(
|
|
343
|
+
{
|
|
344
|
+
"t": "metrics",
|
|
345
|
+
"data": {
|
|
346
|
+
"speak_to_first_frame": speak_to_first_frame_latency
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
text = result.get("text")
|
|
352
|
+
if text:
|
|
353
|
+
await websocket.send_json(
|
|
354
|
+
{
|
|
355
|
+
"t": "bot_text_chunk",
|
|
356
|
+
"data": {"text": text},
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# 发送音频数据给前端
|
|
361
|
+
await websocket.send_json(
|
|
362
|
+
{
|
|
363
|
+
"t": "response",
|
|
364
|
+
"data": data, # base64 编码的音频数据
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
elif result_type in ["complete", "end"]:
|
|
369
|
+
# 处理完成
|
|
370
|
+
logger.info(f"[Live Chat] Bot 回复完成: {bot_text}")
|
|
371
|
+
|
|
372
|
+
# 如果没有音频流,发送 bot 消息文本
|
|
373
|
+
if not audio_playing:
|
|
374
|
+
await websocket.send_json(
|
|
375
|
+
{
|
|
376
|
+
"t": "bot_msg",
|
|
377
|
+
"data": {
|
|
378
|
+
"text": bot_text,
|
|
379
|
+
"ts": int(time.time() * 1000),
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# 发送结束标记
|
|
385
|
+
await websocket.send_json({"t": "end"})
|
|
386
|
+
|
|
387
|
+
# 发送总耗时
|
|
388
|
+
wav_to_tts_duration = time.time() - wav_assembly_finish_time
|
|
389
|
+
await websocket.send_json(
|
|
390
|
+
{
|
|
391
|
+
"t": "metrics",
|
|
392
|
+
"data": {"wav_to_tts_total_time": wav_to_tts_duration},
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
break
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
logger.error(f"[Live Chat] 处理音频失败: {e}", exc_info=True)
|
|
399
|
+
await websocket.send_json({"t": "error", "data": f"处理失败: {str(e)}"})
|
|
400
|
+
|
|
401
|
+
finally:
|
|
402
|
+
session.is_processing = False
|
|
403
|
+
session.should_interrupt = False
|
|
404
|
+
|
|
405
|
+
async def _save_interrupted_message(
|
|
406
|
+
self, session: LiveChatSession, user_text: str, bot_text: str
|
|
407
|
+
):
|
|
408
|
+
"""保存被打断的消息"""
|
|
409
|
+
interrupted_text = bot_text + " [用户打断]"
|
|
410
|
+
logger.info(f"[Live Chat] 保存打断消息: {interrupted_text}")
|
|
411
|
+
|
|
412
|
+
# 简单记录到日志,实际保存逻辑可以后续完善
|
|
413
|
+
try:
|
|
414
|
+
timestamp = int(time.time() * 1000)
|
|
415
|
+
logger.info(
|
|
416
|
+
f"[Live Chat] 用户消息: {user_text} (session: {session.session_id}, ts: {timestamp})"
|
|
417
|
+
)
|
|
418
|
+
if bot_text:
|
|
419
|
+
logger.info(
|
|
420
|
+
f"[Live Chat] Bot 消息(打断): {interrupted_text} (session: {session.session_id}, ts: {timestamp})"
|
|
421
|
+
)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.error(f"[Live Chat] 记录消息失败: {e}", exc_info=True)
|