websocket_handler.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. """
  2. WebSocket处理器模块
  3. """
  4. import json
  5. import asyncio
  6. import time
  7. from typing import Dict, Optional
  8. from fastapi import WebSocket, WebSocketDisconnect
  9. from loguru import logger
  10. from asyncio import Queue
  11. from config import settings
  12. from connection_pool import connection_pool
  13. from error_codes import ErrorCode, get_error_message
  14. from performance import performance_monitor
  15. class StreamingSession:
  16. """流式会话处理"""
  17. def __init__(self, websocket: WebSocket, session_id: str):
  18. self.websocket = websocket
  19. self.session_id = session_id
  20. self.doubao_client = None
  21. self.is_streaming = False
  22. self.is_recording = False
  23. # 音频数据队列
  24. self.to_doubao_queue: Queue = Queue()
  25. self.to_client_queue: Queue = Queue()
  26. # 性能指标
  27. self.start_time = 0
  28. self.audio_received_bytes = 0
  29. self.audio_sent_bytes = 0
  30. # 详细性能时间点
  31. self.perf_recording_start = 0 # 开始录音时间
  32. self.perf_recording_stop = 0 # 停止录音时间
  33. self.perf_asr_complete = 0 # ASR识别完成时间
  34. self.perf_llm_first_token = 0 # LLM首个token时间
  35. self.perf_tts_first_audio = 0 # TTS首个音频包时间
  36. self.perf_audio_complete = 0 # 音频传输完成时间
  37. # 文本收集(用于日志)
  38. self.llm_text_chunks = [] # 收集LLM流式文本
  39. self.asr_text = '' # 最后一次ASR识别结果
  40. # 对话轮次控制
  41. self.response_ended = False # 标记当前轮次是否已结束(避免重复发送结束消息)
  42. # 心跳检测
  43. self.last_ping_time = time.time()
  44. self.heartbeat_timeout = 60 # 60秒无心跳则断开
  45. async def start(self):
  46. """启动会话"""
  47. try:
  48. # 从连接池获取豆包连接
  49. self.doubao_client = await connection_pool.acquire(timeout=5)
  50. if not self.doubao_client:
  51. await self.send_error(
  52. ErrorCode.CONNECTION_POOL_EXHAUSTED,
  53. "连接池已耗尽,请稍后重试"
  54. )
  55. return False
  56. logger.info(f"会话 {self.session_id} 已启动")
  57. return True
  58. except Exception as e:
  59. logger.error(f"启动会话失败: {e}")
  60. # 如果获取连接后出错,需要立即释放
  61. if self.doubao_client:
  62. await connection_pool.release(self.doubao_client)
  63. self.doubao_client = None
  64. await self.send_error(ErrorCode.INTERNAL_ERROR, f"启动会话失败: {str(e)}")
  65. return False
  66. async def process_stream(self):
  67. """处理流式数据"""
  68. try:
  69. self.is_streaming = True
  70. # 并发处理多个任务
  71. await asyncio.gather(
  72. self.receive_from_client(),
  73. self.send_to_doubao(),
  74. self.receive_from_doubao(),
  75. self.send_to_client(),
  76. self.check_heartbeat()
  77. )
  78. except Exception as e:
  79. logger.error(f"流式处理错误: {e}")
  80. await self.send_error(ErrorCode.INTERNAL_ERROR, f"流式处理错误: {str(e)}")
  81. finally:
  82. self.is_streaming = False
  83. async def receive_from_client(self):
  84. """接收客户端数据"""
  85. try:
  86. while self.is_streaming:
  87. message = await self.websocket.receive()
  88. if 'text' in message:
  89. # 控制消息
  90. await self.handle_control_message(message['text'])
  91. elif 'bytes' in message:
  92. # 音频数据
  93. if self.is_recording:
  94. audio_data = message['bytes']
  95. chunk_size = len(audio_data)
  96. self.audio_received_bytes += chunk_size
  97. performance_monitor.record_audio_received(chunk_size)
  98. # 只在DEBUG模式下记录每个音频块
  99. logger.debug(f"收到音频块: {chunk_size} bytes, 累计: {self.audio_received_bytes} bytes")
  100. await self.to_doubao_queue.put(audio_data)
  101. except WebSocketDisconnect:
  102. logger.info(f"客户端 {self.session_id} 断开连接")
  103. self.is_streaming = False
  104. except Exception as e:
  105. logger.error(f"接收客户端数据错误: {e}")
  106. self.is_streaming = False
  107. async def send_to_doubao(self):
  108. """转发音频到豆包API"""
  109. try:
  110. while self.is_streaming:
  111. audio_chunk = await self.to_doubao_queue.get()
  112. if self.doubao_client:
  113. # 转发给豆包API
  114. chunk_size = len(audio_chunk)
  115. await self.doubao_client.send_audio(audio_chunk)
  116. # 使用实例变量中的累计值(在handle_start_recording中重置)
  117. logger.debug(f"转发音频到豆包: {chunk_size} bytes")
  118. except Exception as e:
  119. logger.error(f"转发豆包错误: {e}")
  120. async def receive_from_doubao(self):
  121. """接收豆包API响应"""
  122. try:
  123. # 持续接收豆包响应(支持多轮对话)
  124. while self.is_streaming:
  125. # 持续接收,不等待录音状态
  126. # 接收当前这一轮的音频
  127. if not self.doubao_client:
  128. await asyncio.sleep(0.1)
  129. continue
  130. try:
  131. # 检查豆包连接是否健康
  132. if not self.doubao_client.is_connected():
  133. logger.warning(f"⚠️ 豆包连接已失效 (has_fatal_error={self.doubao_client.has_fatal_error}),停止接收循环")
  134. # 🔑 退出循环,等待下一轮录音时自动重连
  135. # 发送idle状态,让前端准备好下次对话
  136. await self.send_control_message({
  137. 'type': 'state_change',
  138. 'payload': {
  139. 'state': 'idle',
  140. 'previousState': 'speaking',
  141. 'reason': 'connection_needs_recovery'
  142. }
  143. })
  144. # 退出循环,避免无限循环占用资源
  145. break
  146. # 接收豆包音频数据,设置响应超时
  147. audio_data = await asyncio.wait_for(
  148. self.doubao_client.receive_audio(),
  149. timeout=settings.DOUBAO_RESPONSE_TIMEOUT
  150. )
  151. # 检查返回值类型
  152. if audio_data is False:
  153. # False表示当前轮次音频结束
  154. # 检查是否已经发送过结束消息(避免重复)
  155. if self.response_ended:
  156. logger.debug("已发送过结束消息,忽略重复的结束事件")
  157. continue
  158. logger.info("当前轮次音频结束,等待下一轮")
  159. # 如果发送过音频,立即发送结束消息
  160. if self.audio_sent_bytes > 0:
  161. # 记录音频完成时间
  162. self.perf_audio_complete = time.time()
  163. # 计算本轮对话的延迟
  164. response_latency = int((time.time() - self.start_time) * 1000) if self.start_time > 0 else 0
  165. # 打印完整的LLM回复文本
  166. full_llm_text = ''.join(self.llm_text_chunks)
  167. logger.info(f"🔊 [AI回复完成] 音频: {self.audio_sent_bytes / 1024:.1f}KB, 延迟: {response_latency}ms")
  168. if full_llm_text:
  169. logger.info(f"💬 [完整回复] {full_llm_text}")
  170. # 打印性能分析报告
  171. self._log_performance_report()
  172. await self.send_control_message({
  173. 'type': 'ai_response_end',
  174. 'payload': {
  175. 'responseId': f'resp_{int(self.start_time)}',
  176. 'audioSize': self.audio_sent_bytes,
  177. 'message': 'AI回复完成'
  178. }
  179. })
  180. await self.send_control_message({
  181. 'type': 'state_change',
  182. 'payload': {
  183. 'state': 'idle',
  184. 'previousState': 'speaking',
  185. 'reason': 'ai_response_completed'
  186. }
  187. })
  188. else:
  189. # 没有发送过音频,说明豆包没有回复
  190. logger.warning("⚠️ 豆包没有返回音频,可能网络问题或API异常")
  191. await self.send_control_message({
  192. 'type': 'ai_response_end',
  193. 'payload': {
  194. 'responseId': f'resp_{int(self.start_time)}',
  195. 'audioSize': 0,
  196. 'message': 'AI无回复'
  197. }
  198. })
  199. await self.send_control_message({
  200. 'type': 'state_change',
  201. 'payload': {
  202. 'state': 'idle',
  203. 'previousState': 'processing',
  204. 'reason': 'ai_no_response'
  205. }
  206. })
  207. # 标记已结束,避免重复发送
  208. self.response_ended = True
  209. # 重置音频计数器,为下一轮做准备
  210. self.audio_sent_bytes = 0
  211. # 不break,继续外层循环等待下一次录音
  212. continue
  213. elif isinstance(audio_data, tuple):
  214. # 处理文本数据
  215. if audio_data[0] == 'llm_text':
  216. # LLM文本片段
  217. self.llm_text_chunks.append(audio_data[1])
  218. elif audio_data[0] == 'asr_text':
  219. # ASR识别文本(保存最新的)
  220. self.asr_text = audio_data[1]
  221. continue
  222. elif audio_data:
  223. # bytes表示音频数据
  224. # 发送AI响应开始消息(仅第一次)
  225. if self.audio_sent_bytes == 0:
  226. # 记录TTS首包时间
  227. self.perf_tts_first_audio = time.time()
  228. await self.send_control_message({
  229. 'type': 'ai_response_start',
  230. 'payload': {
  231. 'responseId': f'resp_{int(time.time())}',
  232. 'message': 'AI开始回复'
  233. }
  234. })
  235. # 发送状态变更为speaking
  236. await self.send_control_message({
  237. 'type': 'state_change',
  238. 'payload': {
  239. 'state': 'speaking',
  240. 'previousState': 'processing',
  241. 'reason': 'ai_started_response'
  242. }
  243. })
  244. # 将音频数据放入队列
  245. await self.to_client_queue.put(audio_data)
  246. self.audio_sent_bytes += len(audio_data)
  247. performance_monitor.record_audio_sent(len(audio_data))
  248. logger.debug(f"收到豆包音频: {len(audio_data)} bytes, 总计: {self.audio_sent_bytes} bytes")
  249. # 监控输出是否过长(超过100KB警告)
  250. if self.audio_sent_bytes > 100 * 1024:
  251. logger.warning(f"⚠️ AI回复过长: {self.audio_sent_bytes / 1024:.1f}KB,可能需要优化prompt")
  252. else:
  253. # None表示其他事件(文本、状态等),继续等待
  254. logger.debug("收到豆包事件,继续等待音频")
  255. await asyncio.sleep(0.05)
  256. except asyncio.TimeoutError:
  257. logger.warning(f"接收豆包音频超时({settings.DOUBAO_RESPONSE_TIMEOUT}秒),发送超时结束消息")
  258. # 超时后发送结束消息
  259. if self.audio_sent_bytes > 0:
  260. logger.info(f"超时但有音频,已发送: {self.audio_sent_bytes} bytes")
  261. await self.send_control_message({
  262. 'type': 'ai_response_end',
  263. 'payload': {
  264. 'responseId': f'resp_{int(self.start_time)}',
  265. 'audioSize': self.audio_sent_bytes,
  266. 'message': 'AI回复完成(超时)'
  267. }
  268. })
  269. else:
  270. logger.warning("超时且无音频,豆包无回复")
  271. await self.send_control_message({
  272. 'type': 'ai_response_end',
  273. 'payload': {
  274. 'responseId': f'resp_{int(self.start_time)}',
  275. 'audioSize': 0,
  276. 'message': 'AI无回复(超时)'
  277. }
  278. })
  279. await self.send_control_message({
  280. 'type': 'state_change',
  281. 'payload': {
  282. 'state': 'idle',
  283. 'previousState': 'processing',
  284. 'reason': 'ai_timeout'
  285. }
  286. })
  287. # 重置音频计数器
  288. self.audio_sent_bytes = 0
  289. continue
  290. except Exception as e:
  291. logger.error(f"接收豆包响应错误: {e}")
  292. async def send_to_client(self):
  293. """发送音频到客户端"""
  294. try:
  295. while self.is_streaming:
  296. audio_chunk = await self.to_client_queue.get()
  297. # 发送音频数据
  298. await self.websocket.send_bytes(audio_chunk)
  299. logger.debug(f"发送音频到客户端: {len(audio_chunk)} bytes")
  300. except Exception as e:
  301. logger.error(f"发送客户端数据错误: {e}")
  302. async def check_heartbeat(self):
  303. """检查心跳超时"""
  304. try:
  305. while self.is_streaming:
  306. await asyncio.sleep(10) # 每10秒检查一次
  307. elapsed = time.time() - self.last_ping_time
  308. if elapsed > self.heartbeat_timeout:
  309. logger.warning(f"会话 {self.session_id} 心跳超时 ({elapsed:.1f}s)")
  310. await self.send_error(ErrorCode.SESSION_EXPIRED, "心跳超时,连接将关闭")
  311. self.is_streaming = False
  312. break
  313. except Exception as e:
  314. logger.error(f"心跳检测错误: {e}")
  315. async def handle_control_message(self, message_text: str):
  316. """处理控制消息"""
  317. try:
  318. message = json.loads(message_text)
  319. msg_type = message.get('type')
  320. logger.info(f"收到控制消息: {msg_type}")
  321. if msg_type == 'init':
  322. await self.handle_init(message)
  323. elif msg_type == 'start_recording':
  324. await self.handle_start_recording(message)
  325. elif msg_type == 'stop_recording':
  326. await self.handle_stop_recording(message)
  327. elif msg_type == 'ping':
  328. # 更新心跳时间
  329. self.last_ping_time = time.time()
  330. await self.send_control_message({
  331. 'type': 'pong',
  332. 'timestamp': int(time.time() * 1000)
  333. })
  334. elif msg_type == 'close':
  335. self.is_streaming = False
  336. else:
  337. logger.warning(f"未知消息类型: {msg_type}")
  338. except json.JSONDecodeError as e:
  339. logger.error(f"JSON解析错误: {e}")
  340. await self.send_error(ErrorCode.INVALID_MESSAGE)
  341. except Exception as e:
  342. logger.error(f"处理控制消息错误: {e}")
  343. await self.send_error(ErrorCode.INTERNAL_ERROR, f"处理消息失败: {str(e)}")
  344. async def handle_init(self, message: dict):
  345. """处理初始化消息"""
  346. await self.send_control_message({
  347. 'type': 'init_ack',
  348. 'id': message.get('id'),
  349. 'payload': {
  350. 'status': 'success',
  351. 'sessionId': self.session_id,
  352. 'config': {
  353. 'maxAudioChunkSize': 8192,
  354. 'heartbeatInterval': 30000
  355. }
  356. }
  357. })
  358. async def handle_start_recording(self, message: dict):
  359. """处理开始录音"""
  360. # 🔑 关键:检查豆包连接是否健康,如果失效则重新获取
  361. if self.doubao_client and (not self.doubao_client.is_connected() or self.doubao_client.has_fatal_error):
  362. logger.warning(f"🔄 豆包连接已失效 (has_fatal_error={self.doubao_client.has_fatal_error}),重新获取连接...")
  363. # 释放旧连接
  364. try:
  365. await self.connection_pool.release(self.doubao_client)
  366. except Exception as e:
  367. logger.error(f"释放失效连接失败: {e}")
  368. # 重新获取新连接
  369. try:
  370. self.doubao_client = await self.connection_pool.acquire()
  371. logger.info(f"✅ 成功获取新的豆包连接: {self.doubao_client.session_id}")
  372. except Exception as e:
  373. logger.error(f"❌ 重新获取豆包连接失败: {e}")
  374. await self.send_error(
  375. ErrorCode.INTERNAL_ERROR,
  376. "语音服务连接失败,请稍后重试"
  377. )
  378. return
  379. self.is_recording = True
  380. self.start_time = time.time()
  381. # 重置音频计数器(新一轮对话)
  382. self.audio_received_bytes = 0
  383. self.audio_sent_bytes = 0
  384. self.llm_text_chunks = [] # 重置文本收集
  385. self.asr_text = '' # 重置ASR文本
  386. self.response_ended = False # 重置结束标志
  387. # 记录性能时间点
  388. self.perf_recording_start = time.time()
  389. self.perf_recording_stop = 0
  390. self.perf_asr_complete = 0
  391. self.perf_llm_first_token = 0
  392. self.perf_tts_first_audio = 0
  393. self.perf_audio_complete = 0
  394. logger.info(f"🎙️ [开始录音] 新一轮对话")
  395. await self.send_control_message({
  396. 'type': 'recording_started',
  397. 'id': message.get('id'),
  398. 'payload': {
  399. 'status': 'ready',
  400. 'message': '准备接收音频数据'
  401. }
  402. })
  403. await self.send_control_message({
  404. 'type': 'state_change',
  405. 'payload': {
  406. 'state': 'listening',
  407. 'previousState': 'idle',
  408. 'reason': 'user_started_recording'
  409. }
  410. })
  411. async def handle_stop_recording(self, message: dict):
  412. """处理停止录音"""
  413. self.is_recording = False
  414. self.perf_recording_stop = time.time()
  415. duration = int((time.time() - self.start_time) * 1000) if self.start_time > 0 else 0
  416. # 记录录音完成信息
  417. logger.info(f"🎤 [录音完成] 时长: {duration}ms, 音频: {self.audio_received_bytes / 1024:.1f}KB")
  418. # 等待ASR完成识别和队列处理(优化:减少到100ms)
  419. await asyncio.sleep(0.1)
  420. # 打印ASR识别的完整文本
  421. if self.asr_text:
  422. logger.info(f"📝 [用户说] {self.asr_text}")
  423. # 发送静音帧让豆包VAD检测到结束
  424. # 豆包配置了 end_smooth_window_ms: 800,需要0.8秒静音
  425. # 发送900ms静音确保触发(16kHz 16bit = 32000 bytes/s)
  426. if self.doubao_client:
  427. try:
  428. silence_duration_ms = 900 # 优化:减少400ms等待时间
  429. silence_bytes = int(16000 * 2 * silence_duration_ms / 1000)
  430. silence_data = b'\x00' * silence_bytes
  431. await self.doubao_client.send_audio(silence_data)
  432. logger.info(f"⚡ 发送{silence_duration_ms}ms静音数据,快速触发VAD结束检测")
  433. except Exception as e:
  434. logger.error(f"发送静音数据失败: {e}")
  435. await self.send_control_message({
  436. 'type': 'recording_stopped',
  437. 'id': message.get('id'),
  438. 'payload': {
  439. 'status': 'success',
  440. 'duration': duration,
  441. 'message': '录音已停止,等待AI回复'
  442. }
  443. })
  444. # 注意:AI响应开始消息会在receive_from_doubao中自动发送
  445. # 这里只发送状态变更
  446. await self.send_control_message({
  447. 'type': 'state_change',
  448. 'payload': {
  449. 'state': 'processing',
  450. 'previousState': 'listening',
  451. 'reason': 'waiting_ai_response'
  452. }
  453. })
  454. def _log_performance_report(self):
  455. """打印性能分析报告"""
  456. if self.perf_recording_stop == 0:
  457. return
  458. # 计算各阶段耗时(毫秒)
  459. recording_duration = int((self.perf_recording_stop - self.perf_recording_start) * 1000)
  460. # ASR延迟(从停止录音到ASR完成)- 由于ASR是流式的,这里用TTS首包作为近似
  461. asr_latency = 0
  462. if self.perf_tts_first_audio > 0:
  463. asr_latency = int((self.perf_tts_first_audio - self.perf_recording_stop) * 1000) - 900 # 减去静音时间
  464. # TTS首包延迟
  465. tts_first_packet = 0
  466. if self.perf_tts_first_audio > 0:
  467. tts_first_packet = int((self.perf_tts_first_audio - self.perf_recording_stop) * 1000)
  468. # 音频传输时长
  469. audio_streaming = 0
  470. if self.perf_audio_complete > 0 and self.perf_tts_first_audio > 0:
  471. audio_streaming = int((self.perf_audio_complete - self.perf_tts_first_audio) * 1000)
  472. # 总延迟
  473. total_latency = 0
  474. if self.perf_audio_complete > 0:
  475. total_latency = int((self.perf_audio_complete - self.perf_recording_stop) * 1000)
  476. logger.info("=" * 60)
  477. logger.info("⚡ 性能分析报告")
  478. logger.info(f"📊 录音时长: {recording_duration}ms")
  479. logger.info(f"📊 ASR处理: ~{asr_latency}ms (估算)")
  480. logger.info(f"📊 TTS首包: {tts_first_packet}ms")
  481. logger.info(f"📊 音频流式: {audio_streaming}ms")
  482. logger.info(f"📊 总延迟: {total_latency}ms (停止录音→回复完成)")
  483. logger.info("=" * 60)
  484. async def send_control_message(self, message: dict):
  485. """发送控制消息"""
  486. try:
  487. if 'timestamp' not in message:
  488. message['timestamp'] = int(time.time() * 1000)
  489. await self.websocket.send_text(json.dumps(message))
  490. except Exception as e:
  491. logger.error(f"发送控制消息失败: {e}")
  492. async def send_error(self, error_code: int, custom_message: str = None):
  493. """发送错误消息"""
  494. message = custom_message or get_error_message(error_code)
  495. await self.send_control_message({
  496. 'type': 'error',
  497. 'payload': {
  498. 'code': error_code,
  499. 'message': message,
  500. 'severity': 'error',
  501. 'recoverable': True
  502. }
  503. })
  504. async def send_metrics(self):
  505. """发送性能指标"""
  506. latency = int((time.time() - self.start_time) * 1000) if self.start_time > 0 else 0
  507. await self.send_control_message({
  508. 'type': 'metrics',
  509. 'payload': {
  510. 'latency': {
  511. 'total': latency,
  512. 'network': 0, # TODO: 实际计算
  513. 'processing': 0, # TODO: 实际计算
  514. 'ai': 0 # TODO: 实际计算
  515. },
  516. 'audio': {
  517. 'received': self.audio_received_bytes,
  518. 'sent': self.audio_sent_bytes
  519. }
  520. }
  521. })
  522. async def cleanup(self):
  523. """清理会话"""
  524. try:
  525. logger.info(f"开始清理会话 {self.session_id}...")
  526. # 记录会话
  527. performance_monitor.record_session()
  528. # 记录延迟
  529. if self.start_time > 0:
  530. latency = (time.time() - self.start_time) * 1000
  531. performance_monitor.record_latency(latency)
  532. # 注意:ai_response_end已在receive_from_doubao中发送,这里不重复发送
  533. # 归还连接到池中(关键!必须释放)
  534. if self.doubao_client:
  535. logger.debug(f"释放会话 {self.session_id} 的豆包连接到连接池")
  536. await connection_pool.release(self.doubao_client)
  537. self.doubao_client = None
  538. else:
  539. logger.warning(f"会话 {self.session_id} 没有豆包连接需要释放")
  540. logger.info(f"会话 {self.session_id} 已清理 - 收到: {self.audio_received_bytes}B, 发送: {self.audio_sent_bytes}B")
  541. except Exception as e:
  542. logger.error(f"清理会话失败: {e}")
  543. # 即使清理失败,也要尝试释放连接
  544. if self.doubao_client:
  545. try:
  546. await connection_pool.release(self.doubao_client)
  547. self.doubao_client = None
  548. logger.info(f"在异常处理中成功释放连接")
  549. except Exception as release_error:
  550. logger.error(f"释放连接失败: {release_error}")
  551. class ConnectionManager:
  552. """连接管理器"""
  553. def __init__(self):
  554. self.active_connections: Dict[str, StreamingSession] = {}
  555. async def connect(self, websocket: WebSocket, session_id: str) -> StreamingSession:
  556. """建立连接"""
  557. await websocket.accept()
  558. session = StreamingSession(websocket, session_id)
  559. self.active_connections[session_id] = session
  560. logger.info(f"新连接建立: {session_id}, 当前活跃连接数: {len(self.active_connections)}")
  561. return session
  562. async def disconnect(self, session_id: str):
  563. """断开连接"""
  564. if session_id in self.active_connections:
  565. session = self.active_connections[session_id]
  566. await session.cleanup()
  567. del self.active_connections[session_id]
  568. logger.info(f"连接已断开: {session_id}, 当前活跃连接数: {len(self.active_connections)}")
  569. def get_session(self, session_id: str) -> Optional[StreamingSession]:
  570. """获取会话"""
  571. return self.active_connections.get(session_id)
  572. # 创建全局连接管理器
  573. connection_manager = ConnectionManager()