diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 42561b9d..98cf5c5c 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -150,31 +150,29 @@ class ConnectionHandler: # 异步初始化 self.executor.submit(self._initialize_components) # tts 消化线程 - tts_priority = threading.Thread(target=self._tts_priority_thread, daemon=True) - tts_priority.start() + self.tts_priority_thread = threading.Thread(target=self._tts_priority_thread, daemon=True) + self.tts_priority_thread.start() # 音频播放 消化线程 - audio_play_priority = threading.Thread(target=self._audio_play_priority_thread, daemon=True) - audio_play_priority.start() + self.audio_play_priority_thread = threading.Thread(target=self._audio_play_priority_thread, daemon=True) + self.audio_play_priority_thread.start() try: async for message in self.websocket: await self._route_message(message) except websockets.exceptions.ConnectionClosed: self.logger.bind(tag=TAG).info("客户端断开连接") - await self.close() except AuthenticationError as e: self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}") - await ws.close() return except Exception as e: stack_trace = traceback.format_exc() self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}") - await ws.close() return finally: await self.memory.save_memory(self.dialogue.dialogue) + await self.close(ws) async def _route_message(self, message): """消息路由""" @@ -501,7 +499,12 @@ class ConnectionHandler: while not self.stop_event.is_set(): text = None try: - future = self.tts_queue.get() + try: + future = self.tts_queue.get(timeout=1) + except queue.Empty: + if self.stop_event.is_set(): + break + continue if future is None: continue text = None @@ -542,7 +545,12 @@ class ConnectionHandler: while not self.stop_event.is_set(): text = None try: - opus_datas, text, text_index = self.audio_play_queue.get() + try: + opus_datas, text, text_index = self.audio_play_queue.get(timeout=1) + except queue.Empty: + if self.stop_event.is_set(): + break + continue future = asyncio.run_coroutine_threadsafe(sendAudioMessage(self, opus_datas, text, text_index), self.loop) future.result() @@ -572,16 +580,41 @@ class ConnectionHandler: self.tts_first_text_index = text_index self.tts_last_text_index = text_index - async def close(self): + async def close(self, ws=None): """资源清理方法""" - # 清理其他资源 - self.stop_event.set() - self.executor.shutdown(wait=False) - if self.websocket: + # 触发停止事件并清理资源 + if self.stop_event: + self.stop_event.set() + + # 立即关闭线程池 + if self.executor: + self.executor.shutdown(wait=False, cancel_futures=True) + self.executor = None + + # 清空任务队列 + self._clear_queues() + + if ws: + await ws.close() + elif self.websocket: await self.websocket.close() self.logger.bind(tag=TAG).info("连接资源已释放") + def _clear_queues(self): + # 清空所有任务队列 + for q in [self.tts_queue, self.audio_play_queue]: + if not q: + continue + while not q.empty(): + try: + q.get_nowait() + except queue.Empty: + continue + q.queue.clear() + # 添加毒丸信号到队列,确保线程退出 + # q.queue.put(None) + def reset_vad_states(self): self.client_audio_buffer = bytes() self.client_have_voice = False diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py index e8056517..9cad9499 100644 --- a/main/xiaozhi-server/core/handle/functionHandler.py +++ b/main/xiaozhi-server/core/handle/functionHandler.py @@ -50,7 +50,8 @@ class FunctionHandler: self.function_registry.register_function("handle_exit_intent") self.function_registry.register_function("plugin_loader") self.function_registry.register_function("get_time") - self.function_registry.register_function("raise_and_lower_the_volume") + self.function_registry.register_function("get_lunar") + self.function_registry.register_function("handle_device") def register_config_functions(self): """注册配置中的函数,可以不同客户端使用不同的配置""" diff --git a/main/xiaozhi-server/plugins_func/functions/get_time.py b/main/xiaozhi-server/plugins_func/functions/get_time.py index c415e4a2..3eb7324c 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_time.py +++ b/main/xiaozhi-server/plugins_func/functions/get_time.py @@ -6,26 +6,59 @@ get_time_function_desc = { "type": "function", "function": { "name": "get_time", - "description": "获取当前日期、时间和农历、黄历等信息", + "description": "获取今天日期或者当前时间信息", 'parameters': {'type': 'object', 'properties': {}, 'required': []} } } - - @register_function('get_time', get_time_function_desc, ToolType.WAIT) def get_time(): """ - 获取当前日期、时间和农历、黄历等信息 + 获取当前的日期时间信息 """ now = datetime.now() current_time = now.strftime("%H:%M:%S") current_date = now.strftime("%Y-%m-%d") current_weekday = now.strftime("%A") - response_text = f"根据以下信息,回应用户的时间查询请求,默认仅回应公历信息;如果用户询问阴历或农历日期,则回应农历日期;如果用户要求提供更多信息,则回应黄历信息\n" + response_text = f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}" + + return ActionResponse(Action.REQLLM, response_text, None) + + +get_lunar_function_desc = { + "type": "function", + "function": { + "name": "get_lunar", + "description": ( + "用于获取今天的阴历/农历和黄历信息。" + "用户可以指定查询内容,如:阴历日期、天干地支、节气、生肖、星座、八字、宜忌等。" + "如果没有指定查询内容,则默认查询干支年和农历日期。" + ), + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "要查询的内容,例如阴历日期、天干地支、节日、节气、生肖、星座、八字、宜忌等" + } + }, + "required": [] + } + } +} +@register_function('get_lunar', get_lunar_function_desc, ToolType.WAIT) +def get_lunar(query): + """ + 用于获取当前的阴历/农历,和天干地支、节气、生肖、星座、八字、宜忌等黄历信息 + """ + now = datetime.now() + current_time = now.strftime("%H:%M:%S") + current_date = now.strftime("%Y-%m-%d") + current_weekday = now.strftime("%A") + response_text = f"根据以下信息回应用户的查询请求,并提供与{query}相关的信息:\n" lunar = cnlunar.Lunar(now, godType='8char') response_text += ( - f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}\n" + f"当前公历日期: {current_date},当前时间: {current_time},星期: {current_weekday}\n" "农历信息:\n" "%s年%s%s\n" % (lunar.lunarYearCn, lunar.lunarMonthCn[:-1], lunar.lunarDayCn) + "干支: %s年 %s月 %s日\n" % (lunar.year8Char, lunar.month8Char, lunar.day8Char) + @@ -45,7 +78,8 @@ def get_time(): "吉神方位: %s\n" % ' '.join(lunar.get_luckyGodsDirection()) + "今日胎神: %s\n" % lunar.get_fetalGod() + "宜: %s\n" % '、'.join(lunar.goodThing[:10]) + - "忌: %s\n" % '、'.join(lunar.badThing[:10]) + "忌: %s\n" % '、'.join(lunar.badThing[:10]) + + "(默认返回干支年和农历日期;仅在要求查询宜忌信息时才返回本日宜忌)" ) return ActionResponse(Action.REQLLM, response_text, None) \ No newline at end of file diff --git a/main/xiaozhi-server/plugins_func/functions/handle_device.py b/main/xiaozhi-server/plugins_func/functions/handle_device.py new file mode 100644 index 00000000..d564562b --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/handle_device.py @@ -0,0 +1,105 @@ +from config.logger import setup_logging +from plugins_func.register import register_function, ToolType, ActionResponse, Action +from core.handle.iotHandle import get_iot_status, send_iot_conn +import asyncio + +TAG = __name__ +logger = setup_logging() + +async def _get_device_status(conn, device_name, device_type, property_name): + """获取设备状态""" + status = await get_iot_status(conn, device_type, property_name) + if status is None: + raise Exception(f"你的设备不支持{device_name}控制") + return status + +async def _set_device_property(conn, device_name, device_type, method_name, property_name, new_value=None, action=None, step=10): + """设置设备属性""" + current_value = await _get_device_status(conn, device_name, device_type, property_name) + + if action == 'raise': + current_value += step + elif action == 'lower': + current_value -= step + elif action == 'set': + if new_value is None: + raise Exception(f"缺少{property_name}参数") + current_value = new_value + + # 限制属性范围在0到100之间 + current_value = max(0, min(100, current_value)) + + await send_iot_conn(conn, device_type, method_name, {property_name: current_value}) + return current_value + +def _handle_device_action(conn, func, success_message, error_message, *args, **kwargs): + """处理设备操作的通用函数""" + future = asyncio.run_coroutine_threadsafe(func(conn, *args, **kwargs), conn.loop) + try: + result = future.result() + logger.bind(tag=TAG).info(f"{success_message}: {result}") + response = f"{success_message}{result}" + return ActionResponse(action=Action.RESPONSE, result=result, response=response) + except Exception as e: + logger.bind(tag=TAG).error(f"{error_message}: {e}") + response = f"{error_message}: {e}" + return ActionResponse(action=Action.RESPONSE, result=None, response=response) + +# 设备控制 +handle_device_function_desc = { + "type": "function", + "function": { + "name": "handle_device", + "description": ( + "用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。" + "比如用户说现在亮度多少,参数为:device_type:Backlight,action:get。" + "比如用户说设置音量为50,参数为:device_type:Speaker,action:set,value:50。" + "比如用户说亮度太高了,参数为:device_type:Backlight,action:lower。" + "比如用户说调大音量,参数为:device_type:Speaker,action:raise。" + ), + "parameters": { + "type": "object", + "properties": { + "device_type": { + "type": "string", + "description": "设备类型,可选值:Speaker(音量),Backlight(亮度)" + }, + "action": { + "type": "string", + "description": "动作名称,可选值:get(获取),set(设置),raise(提高),lower(降低)" + }, + "value": { + "type": "int", + "description": "值大小,可选值:0-100之间的整数" + } + }, + "required": ["device_type", "action"] + } + } +} + +@register_function('handle_device', handle_device_function_desc, ToolType.IOT_CTL) +def handle_device(conn, device_type: str, action: str, value: int = None): + if device_type == "Speaker": + method_name, property_name, device_name = "SetVolume", "volume", "音量" + elif device_type == "Backlight": + method_name, property_name, device_name = "SetBrightness", "brightness", "亮度" + else: + raise Exception(f"未识别的设备类型: {device_type}") + + if action not in ["get", "set", "raise", "lower"]: + raise Exception(f"未识别的动作名称: {action}") + + if action == "get": + # get + return _handle_device_action( + conn, _get_device_status, f"当前{device_name}", f"获取{device_name}失败", + device_name=device_name, device_type=device_type, property_name=property_name, + ) + else: + # set, raise, lower + return _handle_device_action( + conn, _set_device_property, f"{device_name}已调整到", f"{device_name}调整失败", + device_name=device_name, device_type=device_type, method_name=method_name, + property_name=property_name, new_value=value, action=action + ) diff --git a/main/xiaozhi-server/plugins_func/functions/raise_and_lower_the_volume.py b/main/xiaozhi-server/plugins_func/functions/raise_and_lower_the_volume.py deleted file mode 100644 index c77cb78b..00000000 --- a/main/xiaozhi-server/plugins_func/functions/raise_and_lower_the_volume.py +++ /dev/null @@ -1,64 +0,0 @@ -from config.logger import setup_logging -from plugins_func.register import register_function, ToolType, ActionResponse, Action -from core.handle.iotHandle import get_iot_status, send_iot_conn -import asyncio - -TAG = __name__ -logger = setup_logging() - -raise_and_lower_the_volume_function_desc = { - "type": "function", - "function": { - "name": "raise_and_lower_the_volume", - "description": "用户觉得声音过高或过低,或者用户想提高或降低音量。比如用户说太大声了,参数为:lower,比如用户说提高音量,参数为:raise", - "parameters": { - "type": "object", - "properties": { - "action": { - "type": "string", - "description": "动作名称,要么是raise,要么是lower" - } - }, - "required": ["action"] - } - } -} - - -@register_function('raise_and_lower_the_volume', raise_and_lower_the_volume_function_desc, ToolType.IOT_CTL) -def raise_and_lower_the_volume(conn, action: str): - """ - 获取当前设备音量 - """ - - future = asyncio.run_coroutine_threadsafe( - _raise_and_lower_the_volume(conn, action), - conn.loop - ) - - try: - new_volume = future.result() # 同步等待异步操作完成 - logger.bind(tag=TAG).info(f"音量操作完成: {new_volume}") - response = f"音量已调整到{new_volume}" - except Exception as e: - logger.bind(tag=TAG).error(f"音量操作失败: {e}") - response = f"音量调整失败: {e}" - - return ActionResponse(action=Action.RESPONSE, result="指令已接收", response=response) - - -async def _raise_and_lower_the_volume(conn, action): - volume = await get_iot_status(conn, "Speaker", "volume") - if volume is None: - raise Exception("你的设备不支持音量控制") - if action == 'raise': - volume += 10 - elif action == 'lower': - volume -= 10 - # 限制音量范围在0到100之间 - if volume < 0: - volume = 0 - elif volume > 100: - volume = 100 - await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": volume}) - return volume