update:删除模型前检查是否有引用 (#1027)

* fix:DoubaoASR去掉转wav环节

* update:删除模型前检查是否有引用

* update:去除print

---------

Co-authored-by: MakerZorky <1053714527zhq@gmail.com>
This commit is contained in:
欣南科技
2025-04-27 22:08:30 +08:00
committed by GitHub
co-authored by MakerZorky
4 changed files with 143 additions and 63 deletions
@@ -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<ModelConfigDao, Mode
private final ModelConfigDao modelConfigDao;
private final ModelProviderService modelProviderService;
private final RedisUtils redisUtils;
private final AgentTemplateDao agentTemplateDao;
private final AgentTemplateService agentTemplateService;
private final AgentDao agentDao;
@Override
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
@@ -75,6 +83,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
// 再保存供应器提供的模型
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
modelConfigEntity.setModelType(modelType);
modelConfigEntity.setIsDefault(0);
modelConfigDao.insert(modelConfigEntity);
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
}
@@ -102,9 +111,64 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
@Override
public void delete(String id) {
// 查看是否是默认
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
}
// 验证是否有引用
checkAgentReference(id);
checkIntentConfigReference(id);
modelConfigDao.deleteById(id);
}
/**
* 检查智能体配置是否有引用
*
* @param modelId 模型ID
*/
private void checkAgentReference(String modelId) {
List<AgentEntity> agents = agentDao.selectList(
new QueryWrapper<AgentEntity>()
.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<ModelConfigEntity> intentConfigs = modelConfigDao.selectList(
new QueryWrapper<ModelConfigEntity>()
.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)) {
@@ -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
@@ -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"
# 添加有效的当前天气参数
+24 -10
View File
@@ -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()]
return [func.description for _, func in self.function_registry.items()]