mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 15:43:54 +08:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1e39da9b8 | ||
|
|
e87fc766d2 | ||
|
|
f47f3b050b | ||
|
|
e3453b1a87 | ||
|
|
6e92d169ec | ||
|
|
ed89c05245 |
+3
-1
@@ -125,7 +125,9 @@ public class DeviceController {
|
|||||||
return new Result<Void>().error("设备不存在");
|
return new Result<Void>().error("设备不存在");
|
||||||
}
|
}
|
||||||
BeanUtils.copyProperties(deviceUpdateDTO, entity);
|
BeanUtils.copyProperties(deviceUpdateDTO, entity);
|
||||||
deviceService.updateById(entity);
|
if (!deviceService.updateById(entity)) {
|
||||||
|
return new Result<Void>().error(ErrorCode.UPDATE_DATA_FAILED);
|
||||||
|
}
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -202,8 +202,8 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
|
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
|
||||||
response.setFirmware(firmware);
|
response.setFirmware(firmware);
|
||||||
} else {
|
} else {
|
||||||
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
// 只有在设备已绑定且明确开启自动升级时才返回固件升级信息
|
||||||
if (deviceById.getAutoUpdate() != 0) {
|
if (Integer.valueOf(1).equals(deviceById.getAutoUpdate())) {
|
||||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||||
@@ -299,6 +299,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||||
vo.setDeviceType(device.getBoard());
|
vo.setDeviceType(device.getBoard());
|
||||||
vo.setBoard(device.getBoard());
|
vo.setBoard(device.getBoard());
|
||||||
|
vo.setAutoUpdate(device.getAutoUpdate());
|
||||||
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
|
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
|
||||||
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
|
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
|
||||||
return vo;
|
return vo;
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ public class UserShowDeviceListVO {
|
|||||||
@Schema(description = "设备别名")
|
@Schema(description = "设备别名")
|
||||||
private String alias;
|
private String alias;
|
||||||
|
|
||||||
@Schema(description = "开启OTA")
|
@Schema(description = "自动更新开关(0关闭/1开启)")
|
||||||
private Integer otaUpgrade;
|
private Integer autoUpdate;
|
||||||
|
|
||||||
@Schema(description = "最近对话时间")
|
@Schema(description = "最近对话时间")
|
||||||
private String recentChatTime;
|
private String recentChatTime;
|
||||||
|
|||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.MessageUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.service.DeviceAddressBookService;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
|
@DisplayName("设备更新接口回归测试")
|
||||||
|
class DeviceControllerTest {
|
||||||
|
|
||||||
|
private static final String DEVICE_ID = "device-id";
|
||||||
|
private static final long USER_ID = 1L;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("数据库未更新时不误报自动升级状态修改成功")
|
||||||
|
void updateFailureIsReturnedToCaller() {
|
||||||
|
DeviceService deviceService = mock(DeviceService.class);
|
||||||
|
DeviceEntity entity = ownedDevice();
|
||||||
|
when(deviceService.selectById(DEVICE_ID)).thenReturn(entity);
|
||||||
|
when(deviceService.updateById(entity)).thenReturn(false);
|
||||||
|
DeviceController controller = controller(deviceService);
|
||||||
|
|
||||||
|
DeviceUpdateDTO update = new DeviceUpdateDTO();
|
||||||
|
update.setAutoUpdate(0);
|
||||||
|
|
||||||
|
try (MockedStatic<SecurityUser> securityUser = mockStatic(SecurityUser.class);
|
||||||
|
MockedStatic<MessageUtils> messageUtils = mockStatic(MessageUtils.class)) {
|
||||||
|
securityUser.when(SecurityUser::getUser).thenReturn(currentUser());
|
||||||
|
messageUtils.when(() -> MessageUtils.getMessage(ErrorCode.UPDATE_DATA_FAILED))
|
||||||
|
.thenReturn("Failed to update data");
|
||||||
|
|
||||||
|
Result<Void> result = controller.updateDeviceInfo(DEVICE_ID, update);
|
||||||
|
|
||||||
|
assertEquals(ErrorCode.UPDATE_DATA_FAILED, result.getCode());
|
||||||
|
assertEquals(0, entity.getAutoUpdate());
|
||||||
|
verify(deviceService).updateById(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceController controller(DeviceService deviceService) {
|
||||||
|
return new DeviceController(
|
||||||
|
deviceService,
|
||||||
|
mock(DeviceAddressBookService.class),
|
||||||
|
mock(RedisUtils.class),
|
||||||
|
mock(SysParamsService.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceEntity ownedDevice() {
|
||||||
|
DeviceEntity entity = new DeviceEntity();
|
||||||
|
entity.setId(DEVICE_ID);
|
||||||
|
entity.setUserId(USER_ID);
|
||||||
|
entity.setAutoUpdate(1);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserDetail currentUser() {
|
||||||
|
UserDetail user = new UserDetail();
|
||||||
|
user.setId(USER_ID);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.aop.framework.ProxyFactory;
|
||||||
|
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
|
@DisplayName("设备自动升级回归测试")
|
||||||
|
class DeviceAutoUpdateTest {
|
||||||
|
|
||||||
|
private static final String MAC_ADDRESS = "00:11:22:33:44:55";
|
||||||
|
private static final String BOARD_TYPE = "test-board";
|
||||||
|
private static final String CURRENT_VERSION = "1.0.0";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("#3299 自动升级关闭时 OTA 响应不包含固件")
|
||||||
|
void disabledAutoUpdateOmitsFirmware() {
|
||||||
|
OtaService otaService = mock(OtaService.class);
|
||||||
|
DeviceServiceImpl service = proxiedService(deviceWithAutoUpdate(0), otaService);
|
||||||
|
|
||||||
|
DeviceReportRespDTO response = service.checkDeviceActive(
|
||||||
|
MAC_ADDRESS, MAC_ADDRESS, deviceReport());
|
||||||
|
|
||||||
|
assertNull(response.getFirmware());
|
||||||
|
verifyNoInteractions(otaService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("自动升级开启时继续执行固件查询")
|
||||||
|
void enabledAutoUpdateChecksFirmware() {
|
||||||
|
OtaService otaService = mock(OtaService.class);
|
||||||
|
when(otaService.getLatestOta(BOARD_TYPE)).thenReturn(null);
|
||||||
|
DeviceServiceImpl service = proxiedService(deviceWithAutoUpdate(1), otaService);
|
||||||
|
|
||||||
|
DeviceReportRespDTO response = service.checkDeviceActive(
|
||||||
|
MAC_ADDRESS, MAC_ADDRESS, deviceReport());
|
||||||
|
|
||||||
|
assertNotNull(response.getFirmware());
|
||||||
|
assertEquals(CURRENT_VERSION, response.getFirmware().getVersion());
|
||||||
|
assertEquals(Constant.INVALID_FIRMWARE_URL, response.getFirmware().getUrl());
|
||||||
|
verify(otaService).getLatestOta(BOARD_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceServiceImpl proxiedService(DeviceEntity device, OtaService otaService) {
|
||||||
|
SysParamsService sysParamsService = mock(SysParamsService.class);
|
||||||
|
when(sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true))
|
||||||
|
.thenReturn("ws://127.0.0.1:8000/xiaozhi/v1/");
|
||||||
|
when(sysParamsService.getValue(Constant.SERVER_AUTH_ENABLED, true)).thenReturn("false");
|
||||||
|
when(sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, true)).thenReturn(null);
|
||||||
|
|
||||||
|
DeviceServiceImpl target = new DeviceServiceImpl(
|
||||||
|
null, null, sysParamsService, null, otaService, null) {
|
||||||
|
@Override
|
||||||
|
public DeviceEntity getDeviceByMacAddress(String macAddress) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||||
|
// No-op: connection timestamps are outside this OTA decision test.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ProxyFactory proxyFactory = new ProxyFactory(target);
|
||||||
|
proxyFactory.setProxyTargetClass(true);
|
||||||
|
proxyFactory.setExposeProxy(true);
|
||||||
|
return (DeviceServiceImpl) proxyFactory.getProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceEntity deviceWithAutoUpdate(int autoUpdate) {
|
||||||
|
DeviceEntity device = new DeviceEntity();
|
||||||
|
device.setId(MAC_ADDRESS);
|
||||||
|
device.setMacAddress(MAC_ADDRESS);
|
||||||
|
device.setBoard(BOARD_TYPE);
|
||||||
|
device.setAutoUpdate(autoUpdate);
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceReportReqDTO deviceReport() {
|
||||||
|
DeviceReportReqDTO.Application application = new DeviceReportReqDTO.Application();
|
||||||
|
application.setVersion(CURRENT_VERSION);
|
||||||
|
DeviceReportReqDTO.BoardInfo board = new DeviceReportReqDTO.BoardInfo();
|
||||||
|
board.setType(BOARD_TYPE);
|
||||||
|
|
||||||
|
DeviceReportReqDTO report = new DeviceReportReqDTO();
|
||||||
|
report.setApplication(application);
|
||||||
|
report.setBoard(board);
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -76,6 +76,25 @@ class DeviceTimeSerializationTest {
|
|||||||
() -> assertTrue(payload.path("createDate").isNull()));
|
() -> assertTrue(payload.path("createDate").isNull()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "自动升级状态 {0}")
|
||||||
|
@ValueSource(ints = { 0, 1 })
|
||||||
|
@DisplayName("#3299 设备列表按 autoUpdate 契约返回真实开关状态")
|
||||||
|
void serializedDeviceContainsAutoUpdateState(int autoUpdate) {
|
||||||
|
DeviceEntity entity = new DeviceEntity();
|
||||||
|
entity.setAutoUpdate(autoUpdate);
|
||||||
|
DeviceServiceImpl deviceService = serviceReturning(entity);
|
||||||
|
|
||||||
|
UserShowDeviceListVO device = deviceService.getUserDeviceList(1L, "agent-id").getFirst();
|
||||||
|
ObjectMapper objectMapper = new WebMvcConfig().jackson2HttpMessageConverter().getObjectMapper();
|
||||||
|
JsonNode payload = objectMapper.valueToTree(device);
|
||||||
|
|
||||||
|
assertAll(
|
||||||
|
() -> assertEquals(autoUpdate, device.getAutoUpdate()),
|
||||||
|
() -> assertEquals(autoUpdate, payload.path("autoUpdate").asInt()),
|
||||||
|
() -> assertTrue(payload.path("otaUpgrade").isMissingNode(),
|
||||||
|
"设备列表不应继续暴露未映射的旧字段 otaUpgrade"));
|
||||||
|
}
|
||||||
|
|
||||||
private DeviceServiceImpl serviceReturning(DeviceEntity entity) {
|
private DeviceServiceImpl serviceReturning(DeviceEntity entity) {
|
||||||
return new DeviceServiceImpl(null, null, null, null, null, null) {
|
return new DeviceServiceImpl(null, null, null, null, null, null) {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -118,7 +118,16 @@ async def process_intent_result(
|
|||||||
|
|
||||||
请根据以上信息回答用户的问题:{original_text}"""
|
请根据以上信息回答用户的问题:{original_text}"""
|
||||||
|
|
||||||
response = conn.intent.replyResult(context_prompt, original_text)
|
# 使用异步调用避免阻塞事件循环,影响其他设备的音频播放
|
||||||
|
try:
|
||||||
|
response = asyncio.run_coroutine_threadsafe(
|
||||||
|
conn.intent.replyResult(context_prompt, original_text),
|
||||||
|
conn.loop,
|
||||||
|
).result()
|
||||||
|
except Exception as e:
|
||||||
|
conn.logger.bind(tag=TAG).error(f"LLM生成回复失败: {e}")
|
||||||
|
response = None
|
||||||
|
if response:
|
||||||
speak_txt(conn, response)
|
speak_txt(conn, response)
|
||||||
|
|
||||||
conn.executor.submit(process_context_result)
|
conn.executor.submit(process_context_result)
|
||||||
@@ -184,7 +193,15 @@ async def process_intent_result(
|
|||||||
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||||
text = result.result
|
text = result.result
|
||||||
conn.dialogue.put(Message(role="tool", content=text))
|
conn.dialogue.put(Message(role="tool", content=text))
|
||||||
llm_result = conn.intent.replyResult(text, original_text)
|
# 使用异步调用避免阻塞事件循环,影响其他设备的音频播放
|
||||||
|
try:
|
||||||
|
llm_result = asyncio.run_coroutine_threadsafe(
|
||||||
|
conn.intent.replyResult(text, original_text),
|
||||||
|
conn.loop,
|
||||||
|
).result()
|
||||||
|
except Exception as e:
|
||||||
|
conn.logger.bind(tag=TAG).error(f"LLM生成回复失败: {e}")
|
||||||
|
llm_result = text
|
||||||
if llm_result is None:
|
if llm_result is None:
|
||||||
llm_result = text
|
llm_result = text
|
||||||
speak_txt(conn, llm_result)
|
speak_txt(conn, llm_result)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
from typing import List, Dict, TYPE_CHECKING
|
from typing import List, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -120,12 +121,18 @@ class IntentProvider(IntentProviderBase):
|
|||||||
)
|
)
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
def replyResult(self, text: str, original_text: str):
|
async def replyResult(self, text: str, original_text: str):
|
||||||
|
"""使用 asyncio.to_thread 避免阻塞事件循环"""
|
||||||
try:
|
try:
|
||||||
llm_result = self.llm.response_no_stream(
|
user_prompt = (
|
||||||
|
"请根据以上内容,像人类一样说话的口吻回复用户,要求简洁,请直接返回结果。用户现在说:"
|
||||||
|
+ original_text
|
||||||
|
)
|
||||||
|
# 使用 to_thread 将同步阻塞调用放到线程池中执行,不阻塞事件循环
|
||||||
|
llm_result = await asyncio.to_thread(
|
||||||
|
self.llm.response_no_stream,
|
||||||
system_prompt=text,
|
system_prompt=text,
|
||||||
user_prompt="请根据以上内容,像人类一样说话的口吻回复用户,要求简洁,请直接返回结果。用户现在说:"
|
user_prompt=user_prompt,
|
||||||
+ original_text,
|
|
||||||
)
|
)
|
||||||
return llm_result
|
return llm_result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -207,8 +214,11 @@ class IntentProvider(IntentProviderBase):
|
|||||||
logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}")
|
logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
intent = self.llm.response_no_stream(
|
# 使用 to_thread 将同步阻塞调用放到线程池中,避免阻塞事件循环
|
||||||
system_prompt=prompt_music, user_prompt=user_prompt
|
intent = await asyncio.to_thread(
|
||||||
|
self.llm.response_no_stream,
|
||||||
|
system_prompt=prompt_music,
|
||||||
|
user_prompt=user_prompt,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in intent detection LLM call: {e}")
|
logger.bind(tag=TAG).error(f"Error in intent detection LLM call: {e}")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import random
|
import random
|
||||||
import httpx
|
import httpx
|
||||||
from markitdown import MarkItDown
|
from io import BytesIO
|
||||||
|
from markitdown import MarkItDown, StreamInfo
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -144,7 +145,14 @@ async def fetch_news_detail(url):
|
|||||||
|
|
||||||
# 使用MarkItDown清理HTML内容
|
# 使用MarkItDown清理HTML内容
|
||||||
md = MarkItDown(enable_plugins=False)
|
md = MarkItDown(enable_plugins=False)
|
||||||
result = md.convert(response)
|
result = md.convert_stream(
|
||||||
|
BytesIO(response.content),
|
||||||
|
stream_info=StreamInfo(
|
||||||
|
mimetype="text/html",
|
||||||
|
extension=".html",
|
||||||
|
charset=response.encoding or "utf-8",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# 获取清理后的文本内容
|
# 获取清理后的文本内容
|
||||||
clean_text = result.text_content
|
clean_text = result.text_content
|
||||||
|
|||||||
Reference in New Issue
Block a user