diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java index 00d48486..25b03034 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java @@ -3,6 +3,7 @@ package xiaozhi.modules.model.service.impl; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -19,6 +20,10 @@ import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; +import xiaozhi.modules.agent.dao.AgentDao; +import xiaozhi.modules.agent.dao.AgentTemplateDao; +import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.model.dao.ModelConfigDao; import xiaozhi.modules.model.dto.ModelBasicInfoDTO; import xiaozhi.modules.model.dto.ModelConfigBodyDTO; @@ -36,6 +41,9 @@ public class ModelConfigServiceImpl extends BaseServiceImpl getModelCodeList(String modelType, String modelName) { @@ -75,6 +83,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl agents = agentDao.selectList( + new QueryWrapper() + .eq("vad_model_id", modelId) + .or() + .eq("asr_model_id", modelId) + .or() + .eq("llm_model_id", modelId) + .or() + .eq("tts_model_id", modelId) + .or() + .eq("mem_model_id", modelId) + .or() + .eq("intent_model_id", modelId)); + if (!agents.isEmpty()) { + String agentNames = agents.stream() + .map(AgentEntity::getAgentName) + .collect(Collectors.joining("、")); + throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames)); + } + } + + /** + * 检查意图识别配置是否有引用 + * + * @param modelId 模型ID + */ + private void checkIntentConfigReference(String modelId) { + ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId); + if (modelConfig != null + && "LLM".equals(modelConfig.getModelType() == null ? null : modelConfig.getModelType().toUpperCase())) { + List intentConfigs = modelConfigDao.selectList( + new QueryWrapper() + .eq("model_type", "Intent") + .like("config_json", "%" + modelId + "%")); + if (!intentConfigs.isEmpty()) { + throw new RenException("该LLM模型已被意图识别配置引用,无法删除"); + } + } + } + @Override public String getModelNameById(String id) { if (StringUtils.isBlank(id)) { diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py index a5a5bb65..b19ceb97 100644 --- a/main/xiaozhi-server/core/providers/asr/doubao.py +++ b/main/xiaozhi-server/core/providers/asr/doubao.py @@ -45,14 +45,14 @@ def parse_response(res): payload 类似与http 请求体 """ protocol_version = res[0] >> 4 - header_size = res[0] & 0x0f + header_size = res[0] & 0x0F message_type = res[1] >> 4 - message_type_specific_flags = res[1] & 0x0f + message_type_specific_flags = res[1] & 0x0F serialization_method = res[2] >> 4 - message_compression = res[2] & 0x0f + message_compression = res[2] & 0x0F reserved = res[3] - header_extensions = res[4:header_size * 4] - payload = res[header_size * 4:] + header_extensions = res[4 : header_size * 4] + payload = res[header_size * 4 :] result = {} payload_msg = None payload_size = 0 @@ -61,13 +61,13 @@ def parse_response(res): payload_msg = payload[4:] elif message_type == SERVER_ACK: seq = int.from_bytes(payload[:4], "big", signed=True) - result['seq'] = seq + result["seq"] = seq if len(payload) >= 8: payload_size = int.from_bytes(payload[4:8], "big", signed=False) payload_msg = payload[8:] elif message_type == SERVER_ERROR_RESPONSE: code = int.from_bytes(payload[:4], "big", signed=False) - result['code'] = code + result["code"] = code payload_size = int.from_bytes(payload[4:8], "big", signed=False) payload_msg = payload[8:] if payload_msg is None: @@ -78,8 +78,8 @@ def parse_response(res): payload_msg = json.loads(str(payload_msg, "utf-8")) elif serialization_method != NO_SERIALIZATION: payload_msg = str(payload_msg, "utf-8") - result['payload_msg'] = payload_msg - result['payload_size'] = payload_size + result["payload_msg"] = payload_msg + result["payload_size"] = payload_size return result @@ -122,7 +122,9 @@ class ASRProvider(ASRProviderBase): return file_path @staticmethod - def _generate_header(message_type=CLIENT_FULL_REQUEST, message_type_specific_flags=NO_SEQUENCE) -> bytearray: + def _generate_header( + message_type=CLIENT_FULL_REQUEST, message_type_specific_flags=NO_SEQUENCE + ) -> bytearray: """Generate protocol header.""" header = bytearray() header_size = 1 @@ -143,13 +145,9 @@ class ASRProvider(ASRProviderBase): "user": { "uid": str(uuid.uuid4()), }, - "request": { - "reqid": reqid, - "show_utterances": False, - "sequence": 1 - }, + "request": {"reqid": reqid, "show_utterances": False, "sequence": 1}, "audio": { - "format": "wav", + "format": "raw", "rate": 16000, "language": "zh-CN", "bits": 16, @@ -158,18 +156,23 @@ class ASRProvider(ASRProviderBase): }, } - async def _send_request(self, audio_data: List[bytes], segment_size: int) -> Optional[str]: + async def _send_request( + self, audio_data: List[bytes], segment_size: int + ) -> Optional[str]: """Send request to Volcano ASR service.""" try: - auth_header = {'Authorization': 'Bearer; {}'.format(self.access_token)} - async with websockets.connect(self.ws_url, additional_headers=auth_header) as websocket: + auth_header = {"Authorization": "Bearer; {}".format(self.access_token)} + async with websockets.connect( + self.ws_url, additional_headers=auth_header + ) as websocket: # Prepare request data request_params = self._construct_request(str(uuid.uuid4())) - print(request_params) payload_bytes = str.encode(json.dumps(request_params)) payload_bytes = gzip.compress(payload_bytes) full_client_request = self._generate_header() - full_client_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes) + full_client_request.extend( + (len(payload_bytes)).to_bytes(4, "big") + ) # payload size(4 bytes) full_client_request.extend(payload_bytes) # payload # Send header and metadata @@ -177,22 +180,29 @@ class ASRProvider(ASRProviderBase): await websocket.send(full_client_request) res = await websocket.recv() result = parse_response(res) - if 'payload_msg' in result and result['payload_msg']['code'] != self.success_code: + if ( + "payload_msg" in result + and result["payload_msg"]["code"] != self.success_code + ): logger.bind(tag=TAG).error(f"ASR error: {result}") return None - for seq, (chunk, last) in enumerate(self.slice_data(audio_data, segment_size), 1): + for seq, (chunk, last) in enumerate( + self.slice_data(audio_data, segment_size), 1 + ): if last: audio_only_request = self._generate_header( message_type=CLIENT_AUDIO_ONLY_REQUEST, - message_type_specific_flags=NEG_SEQUENCE + message_type_specific_flags=NEG_SEQUENCE, ) else: audio_only_request = self._generate_header( message_type=CLIENT_AUDIO_ONLY_REQUEST ) payload_bytes = gzip.compress(chunk) - audio_only_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes) + audio_only_request.extend( + (len(payload_bytes)).to_bytes(4, "big") + ) # payload size(4 bytes) audio_only_request.extend(payload_bytes) # payload # Send audio data await websocket.send(audio_only_request) @@ -201,9 +211,12 @@ class ASRProvider(ASRProviderBase): response = await websocket.recv() result = parse_response(response) - if 'payload_msg' in result and result['payload_msg']['code'] == self.success_code: - if len(result['payload_msg']['result']) > 0: - return result['payload_msg']['result'][0]["text"] + if ( + "payload_msg" in result + and result["payload_msg"]["code"] == self.success_code + ): + if len(result["payload_msg"]["result"]) > 0: + return result["payload_msg"]["result"][0]["text"] return None else: logger.bind(tag=TAG).error(f"ASR error: {result}") @@ -231,7 +244,7 @@ class ASRProvider(ASRProviderBase): @staticmethod def read_wav_info(data: io.BytesIO = None) -> (int, int, int, int, int): with io.BytesIO(data) as _f: - wave_fp = wave.open(_f, 'rb') + wave_fp = wave.open(_f, "rb") nchannels, sampwidth, framerate, nframes = wave_fp.getparams()[:4] wave_bytes = wave_fp.readframes(nframes) return nchannels, sampwidth, framerate, nframes, len(wave_bytes) @@ -247,37 +260,32 @@ class ASRProvider(ASRProviderBase): data_len = len(data) offset = 0 while offset + chunk_size < data_len: - yield data[offset: offset + chunk_size], False + yield data[offset : offset + chunk_size], False offset += chunk_size else: - yield data[offset: data_len], True + yield data[offset:data_len], True - async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + async def speech_to_text( + self, opus_data: List[bytes], session_id: str + ) -> Tuple[Optional[str], Optional[str]]: """将语音数据转换为文本""" try: # 合并所有opus数据包 pcm_data = self.decode_opus(opus_data, session_id) - combined_pcm_data = b''.join(pcm_data) + combined_pcm_data = b"".join(pcm_data) - wav_buffer = io.BytesIO() - - with wave.open(wav_buffer, "wb") as wav_file: - wav_file.setnchannels(1) # 设置声道数 - wav_file.setsampwidth(2) # 设置采样宽度 - wav_file.setframerate(16000) # 设置采样率 - wav_file.writeframes(combined_pcm_data) # 写入 PCM 数据 - - # 获取封装后的 WAV 数据 - wav_data = wav_buffer.getvalue() - nchannels, sampwidth, framerate, nframes, wav_len = self.read_wav_info(wav_data) - size_per_sec = nchannels * sampwidth * framerate + # 直接使用PCM数据 + # 计算分段大小 (单声道, 16bit, 16kHz采样率) + size_per_sec = 1 * 2 * 16000 # nchannels * sampwidth * framerate segment_size = int(size_per_sec * self.seg_duration / 1000) # 语音识别 start_time = time.time() - text = await self._send_request(wav_data, segment_size) + text = await self._send_request(combined_pcm_data, segment_size) if text: - logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}") + logger.bind(tag=TAG).debug( + f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}" + ) return text, None return "", None diff --git a/main/xiaozhi-server/plugins_func/functions/get_weather.py b/main/xiaozhi-server/plugins_func/functions/get_weather.py index b17755a2..dc7976dd 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_weather.py +++ b/main/xiaozhi-server/plugins_func/functions/get_weather.py @@ -153,21 +153,17 @@ def parse_weather_info(soup): def get_weather(conn, location: str = None, lang: str = "zh_CN"): api_key = conn.config["plugins"]["get_weather"]["api_key"] default_location = conn.config["plugins"]["get_weather"]["default_location"] - print("用户设置的default_location为:", default_location) client_ip = conn.client_ip - print("用户提供的location为:", location) # 优先使用用户提供的location参数 if not location: # 通过客户端IP解析城市 if client_ip: # 动态解析IP对应的城市信息 ip_info = get_ip_info(client_ip, logger) - print("解析用户IP后的城市为:", ip_info) location = ip_info.get("city") if ip_info and "city" in ip_info else None else: # 若IP解析失败或无IP,使用默认位置 location = default_location - print("解析失败,将使用默认地址", location) city_info = fetch_city_info(location, api_key) if not city_info: @@ -179,8 +175,6 @@ def get_weather(conn, location: str = None, lang: str = "zh_CN"): return ActionResponse(Action.REQLLM, None, "请求失败") city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup) - print(f"当前查询的城市名称是: {city_name}") - weather_report = f"您查询的位置是:{city_name}\n\n当前天气: {current_abstract}\n" # 添加有效的当前天气参数 diff --git a/main/xiaozhi-server/plugins_func/register.py b/main/xiaozhi-server/plugins_func/register.py index fd990760..b96f2f29 100644 --- a/main/xiaozhi-server/plugins_func/register.py +++ b/main/xiaozhi-server/plugins_func/register.py @@ -10,7 +10,10 @@ class ToolType(Enum): NONE = (1, "调用完工具后,不做其他操作") WAIT = (2, "调用工具,等待函数返回") CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责") - SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数") + SYSTEM_CTL = ( + 4, + "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数", + ) IOT_CTL = (5, "IOT设备控制,需要传递conn参数") MCP_CLIENT = (6, "MCP客户端") @@ -30,12 +33,14 @@ class Action(Enum): self.code = code self.message = message + class ActionResponse: def __init__(self, action: Action, result, response): self.action = action # 动作类型 self.result = result # 动作产生的结果 self.response = response # 直接回复的内容 + class FunctionItem: def __init__(self, name, description, func, type): self.name = name @@ -43,40 +48,49 @@ class FunctionItem: self.func = func self.type = type + class DeviceTypeRegistry: """设备类型注册表,用于管理IOT设备类型及其函数""" + def __init__(self): self.type_functions = {} # type_signature -> {func_name: FunctionItem} - + def generate_device_type_id(self, descriptor): """通过设备能力描述生成类型ID""" properties = sorted(descriptor["properties"].keys()) methods = sorted(descriptor["methods"].keys()) # 使用属性和方法的组合作为设备类型的唯一标识 - type_signature = f"{descriptor['name']}:{','.join(properties)}:{','.join(methods)}" + type_signature = ( + f"{descriptor['name']}:{','.join(properties)}:{','.join(methods)}" + ) return type_signature - + def get_device_functions(self, type_id): """获取设备类型对应的所有函数""" return self.type_functions.get(type_id, {}) - + def register_device_type(self, type_id, functions): """注册设备类型及其函数""" if type_id not in self.type_functions: self.type_functions[type_id] = functions + # 初始化函数注册字典 all_function_registry = {} device_type_registry = DeviceTypeRegistry() + def register_function(name, desc, type=None): """注册函数到函数注册字典的装饰器""" + def decorator(func): all_function_registry[name] = FunctionItem(name, desc, func, type) logger.bind(tag=TAG).debug(f"函数 '{name}' 已加载,可以注册使用") return func + return decorator + class FunctionRegistry: def __init__(self): self.function_registry = {} @@ -89,9 +103,9 @@ class FunctionRegistry: self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到") return None self.function_registry[name] = func - self.logger.bind(tag=TAG).info(f"函数 '{name}' 注册成功") + self.logger.bind(tag=TAG).debug(f"函数 '{name}' 注册成功") return func - + def unregister_function(self, name): # 注销函数,检测是否存在 if name not in self.function_registry: @@ -103,9 +117,9 @@ class FunctionRegistry: def get_function(self, name): return self.function_registry.get(name) - + def get_all_functions(self): return self.function_registry - + def get_all_function_desc(self): - return [func.description for _, func in self.function_registry.items()] \ No newline at end of file + return [func.description for _, func in self.function_registry.items()]