update:websocket播报6位验证码 (#821)

This commit is contained in:
hrz
2025-04-15 18:30:12 +08:00
committed by GitHub
parent 4e885dac3e
commit a83410cb49
18 changed files with 183 additions and 57 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ server:
```
智控台地址: https://2662r3426b.vicp.fun
OTA接口地址: htts://2662r3426b.vicp.fun/xiaozhi/ota/
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
```
---
@@ -52,4 +52,7 @@ public interface ErrorCode {
int PARAM_BOOLEAN_INVALID = 10038;
int PARAM_ARRAY_INVALID = 10039;
int PARAM_JSON_INVALID = 10040;
int OTA_DEVICE_NOT_FOUND = 10041;
int OTA_DEVICE_NEED_BIND = 10042;
}
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
@@ -83,7 +84,12 @@ public class ConfigServiceImpl implements ConfigService {
// 根据MAC地址查找设备
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null) {
throw new RenException("设备未找到");
// 如果设备,去redis里看看有没有需要连接的设备
String cachedCode = deviceService.geCodeByDeviceId(macAddress);
if (StringUtils.isNotBlank(cachedCode)) {
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
}
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
}
// 获取智能体信息
AgentEntity agent = agentService.getAgentById(device.getAgentId());
@@ -37,9 +37,7 @@ public class OTAController {
@PostMapping
public ResponseEntity<String> checkOTAVersion(
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
if (StringUtils.isAnyBlank(deviceId, clientId)) {
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
@@ -50,10 +48,9 @@ public class OTAController {
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
}
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
}
@Operation(summary = "获取 OTA 提示信息")
@GetMapping
public ResponseEntity<String> getOTAPrompt() {
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
@@ -19,7 +19,7 @@ public interface DeviceService {
/**
* 检查设备是否激活
*/
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
DeviceReportReqDTO deviceReport);
/**
@@ -74,4 +74,12 @@ public interface DeviceService {
* @return 设备信息
*/
DeviceEntity getDeviceByMacAddress(String macAddress);
/**
* 根据设备ID获取激活码
*
* @param deviceId 设备ID
* @return 激活码
*/
String geCodeByDeviceId(String deviceId);
}
@@ -120,7 +120,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
@Override
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
DeviceReportReqDTO deviceReport) {
DeviceReportRespDTO response = new DeviceReportRespDTO();
response.setServer_time(buildServerTime());
@@ -132,46 +132,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
response.setFirmware(firmware);
DeviceEntity deviceById = getDeviceById(deviceId);
DeviceEntity deviceById = getDeviceById(macAddress);
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
deviceById.setLastConnectedAt(new Date());
deviceDao.updateById(deviceById);
} else { // 如果设备不存在,则生成激活码
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code");
code.setCode(cachedCode);
code.setMessage(frontedUrl + "\n" + cachedCode);
} else {
String newCode = RandomUtil.randomNumbers(6);
code.setCode(newCode);
code.setMessage(frontedUrl + "\n" + newCode);
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("id", deviceId);
dataMap.put("mac_address", macAddress);
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
dataMap.put("app_version", (deviceReport.getApplication() != null)
? deviceReport.getApplication().getVersion()
: null);
dataMap.put("deviceId", deviceId);
dataMap.put("activation_code", newCode);
// 写入主数据 key
redisTemplate.opsForHash().putAll(dataKey, dataMap);
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
// 写入反查激活码 key
String codeKey = "ota:activation:code:" + newCode;
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
}
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
response.setActivation(code);
}
@@ -258,4 +224,60 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
return serverTime;
}
@Override
public String geCodeByDeviceId(String deviceId) {
String dataKey = getDeviceCacheKey(deviceId);
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
String cachedCode = (String) cacheMap.get("activation_code");
return cachedCode;
}
return null;
}
private String getDeviceCacheKey(String deviceId) {
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
return dataKey;
}
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
String cachedCode = geCodeByDeviceId(deviceId);
if (StringUtils.isNotBlank(cachedCode)) {
code.setCode(cachedCode);
code.setMessage(frontedUrl + "\n" + cachedCode);
} else {
String newCode = RandomUtil.randomNumbers(6);
code.setCode(newCode);
code.setMessage(frontedUrl + "\n" + newCode);
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("id", deviceId);
dataMap.put("mac_address", deviceId);
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
dataMap.put("app_version", (deviceReport.getApplication() != null)
? deviceReport.getApplication().getVersion()
: null);
dataMap.put("deviceId", deviceId);
dataMap.put("activation_code", newCode);
// 写入主数据 key
String dataKey = getDeviceCacheKey(deviceId);
redisTemplate.opsForHash().putAll(dataKey, dataMap);
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
// 写入反查激活码 key
String codeKey = "ota:activation:code:" + newCode;
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
}
return code;
}
}
@@ -41,3 +41,6 @@
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0}
@@ -40,4 +40,7 @@
10037=Parameter value must be a valid number
10038=Parameter value must be true or false
10039=Parameter value must be a valid JSON array format
10040=Parameter value must be a valid JSON format
10040=Parameter value must be a valid JSON format
10041=Device not found
10042={0}
@@ -40,4 +40,7 @@
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0}
@@ -40,4 +40,7 @@
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=\u8A2D\u5099\u672A\u627E\u5230
10042={0}
@@ -33,3 +33,5 @@ timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
ota.device.not.found=\u8BBE\u5907\u672A\u627E\u5230
ota.device.need.bind={0}
@@ -32,3 +32,5 @@ timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
ota.device.not.found=Device not found
ota.device.need.bind={0}
@@ -32,3 +32,5 @@ timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u7
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
ota.device.need.bind={0}
@@ -31,3 +31,6 @@ timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
ota.device.need.bind={0}
+16 -1
View File
@@ -4,6 +4,17 @@ import requests
import yaml
import time
class DeviceNotFoundException(Exception):
pass
class DeviceBindException(Exception):
def __init__(self, bind_code):
self.bind_code = bind_code
super().__init__(f"设备绑定异常,绑定码: {bind_code}")
# 添加全局配置缓存
_config_cache = None
@@ -83,7 +94,11 @@ def _make_api_request(api_url, secret, endpoint, json_data=None):
response = requests.post(f"{api_url}{endpoint}", json=json_data)
if response.status_code == 200:
result = response.json()
if result.get("code") != 0:
if result.get("code") == 10041:
raise DeviceNotFoundException(result.get("msg"))
elif result.get("code") == 10042:
raise DeviceBindException(result.get("msg"))
elif result.get("code") != 0:
raise Exception(f"API返回错误: {result.get('msg', '未知错误')}")
return result.get("data")
+16 -5
View File
@@ -27,7 +27,11 @@ from core.handle.functionHandler import FunctionHandler
from plugins_func.register import Action, ActionResponse
from core.auth import AuthMiddleware, AuthenticationError
from core.mcp.manager import MCPManager
from config.config_loader import get_private_config_from_api
from config.config_loader import (
get_private_config_from_api,
DeviceNotFoundException,
DeviceBindException,
)
TAG = __name__
@@ -46,6 +50,9 @@ class ConnectionHandler:
self.logger = setup_logging()
self.auth = AuthMiddleware(config)
self.need_bind = False
self.bind_code = None
self.websocket = None
self.headers = None
self.client_ip = None
@@ -206,7 +213,15 @@ class ConnectionHandler:
)
private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}")
except DeviceNotFoundException as e:
self.need_bind = True
private_config = {}
except DeviceBindException as e:
self.need_bind = True
self.bind_code = e.bind_code
private_config = {}
except Exception as e:
self.need_bind = True
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
private_config = {}
@@ -345,7 +360,6 @@ class ConnectionHandler:
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
try:
start_time = time.time()
# 使用带记忆的对话
future = asyncio.run_coroutine_threadsafe(
self.memory.query_memory(query), self.loop
@@ -367,9 +381,6 @@ class ConnectionHandler:
if self.client_abort:
break
end_time = time.time()
# self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
# 合并当前全部文本并处理未分割部分
full_text = "".join(response_message)
current_text = full_text[processed_chars:] # 从未处理的位置开始
@@ -1,5 +1,6 @@
from config.logger import setup_logging
import time
import asyncio
from core.utils.util import remove_punctuation_and_length
from core.handle.sendAudioHandle import send_stt_message
from core.handle.intentHandler import handle_user_intent
@@ -35,9 +36,7 @@ async def handleAudioMessage(conn, audio):
if len(conn.asr_audio) < 15:
conn.asr_server_receive = True
else:
text, file_path = await conn.asr.speech_to_text(
conn.asr_audio, conn.session_id
)
text, _ = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
logger.bind(tag=TAG).info(f"识别文本: {text}")
text_len, _ = remove_punctuation_and_length(text)
if text_len > 0:
@@ -49,6 +48,9 @@ async def handleAudioMessage(conn, audio):
async def startToChat(conn, text):
if conn.need_bind:
await check_bind_device(conn)
return
# 首先进行意图分析
intent_handled = await handle_user_intent(conn, text)
@@ -85,3 +87,44 @@ async def no_voice_close_connect(conn):
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
)
await startToChat(conn, prompt)
async def check_bind_device(conn):
if conn.bind_code:
# 确保bind_code是6位数字
if len(conn.bind_code) != 6:
logger.bind(tag=TAG).error(f"无效的绑定码格式: {conn.bind_code}")
text = "绑定码格式错误,请检查配置。"
await send_stt_message(conn, text)
return
text = f"请登录控制面板,输入{conn.bind_code},绑定设备。"
await send_stt_message(conn, text)
conn.tts_first_text_index = 0
conn.tts_last_text_index = 6
conn.llm_finish_task = True
# 播放提示音
music_path = "config/assets/bind_code.wav"
opus_packets, _ = conn.tts.audio_to_opus_data(music_path)
conn.audio_play_queue.put((opus_packets, text, 0))
# 逐个播放数字
for i in range(6): # 确保只播放6位数字
try:
digit = conn.bind_code[i]
num_path = f"config/assets/bind_code/{digit}.wav"
num_packets, _ = conn.tts.audio_to_opus_data(num_path)
conn.audio_play_queue.put((num_packets, text, i + 1))
except Exception as e:
logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
continue
else:
text = f"没有找到该设备的版本信息,请正确配置 OTA地址,然后重新编译固件。"
await send_stt_message(conn, text)
conn.tts_first_text_index = 0
conn.tts_last_text_index = 0
conn.llm_finish_task = True
music_path = "config/assets/bind_not_found.wav"
opus_packets, _ = conn.tts.audio_to_opus_data(music_path)
conn.audio_play_queue.put((opus_packets, text, 0))
@@ -16,7 +16,7 @@ class TTSProvider(TTSProviderBase):
self.voice = config.get("voice")
self.response_format = config.get("response_format")
self.sample_rate = config.get("sample_rate")
self.speed = float(config.get("speed"))
self.speed = float(config.get("speed", 1.0))
self.gain = config.get("gain")
self.host = "api.siliconflow.cn"